Fix client side team bugs (#86)

* permission ids -> definition jsons

* fixed default permission update bug

* fixed set team default permission

* fixed handler edit dialog
This commit is contained in:
Zai Shi 2024-06-20 15:28:23 +02:00 committed by GitHub
parent 3cd4d3f176
commit bd96da663b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 99 additions and 58 deletions

View File

@ -46,7 +46,7 @@ const teamSystemPermissionDescriptionMap: Record<DBTeamSystemPermission, string>
"INVITE_MEMBERS": "Invite other users to the team",
};
function serverPermissionDefinitionJsonFromDbType(
export function serverPermissionDefinitionJsonFromDbType(
db: Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>
): ServerPermissionDefinitionJson {
if (!db.projectConfigId && !db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId`, { db });
@ -74,7 +74,7 @@ function serverPermissionDefinitionJsonFromDbType(
};
}
function serverPermissionDefinitionJsonFromTeamSystemDbType(
export function serverPermissionDefinitionJsonFromTeamSystemDbType(
db: DBTeamSystemPermission,
): ServerPermissionDefinitionJson {
return {

View File

@ -8,7 +8,7 @@ import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
import { EmailConfigJson, SharedProvider, StandardProvider, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/interface/clientInterface";
import { OAuthProviderUpdateOptions, ProjectUpdateOptions } from "@stackframe/stack-shared/dist/interface/adminInterface";
import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { isTeamSystemPermission, listServerPermissionDefinitions, teamDBTypeToSystemPermissionString, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions";
import { fullPermissionInclude, isTeamSystemPermission, listServerPermissionDefinitions, serverPermissionDefinitionJsonFromDbType, serverPermissionDefinitionJsonFromTeamSystemDbType, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions";
function toDBSharedProvider(type: SharedProvider): ProxiedOAuthProviderType {
@ -67,7 +67,9 @@ export const fullProjectInclude = {
standardEmailServiceConfig: true,
},
},
permissions: true,
permissions: {
include: fullPermissionInclude,
},
domains: true,
},
},
@ -660,12 +662,12 @@ export function projectJsonFromDbType(project: ProjectDB): ProjectJson {
return [];
}),
emailConfig,
teamCreatorDefaultPermissionIds: project.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission)
.map((perm) => perm.queryableId)
.concat(project.config.teamCreateDefaultSystemPermissions.map(teamDBTypeToSystemPermissionString)),
teamMemberDefaultPermissionIds: project.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission)
.map((perm) => perm.queryableId)
.concat(project.config.teamMemberDefaultSystemPermissions.map(teamDBTypeToSystemPermissionString)),
teamCreatorDefaultPermissions: project.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission)
.map(serverPermissionDefinitionJsonFromDbType)
.concat(project.config.teamCreateDefaultSystemPermissions.map(serverPermissionDefinitionJsonFromTeamSystemDbType)),
teamMemberDefaultPermissions: project.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission)
.map(serverPermissionDefinitionJsonFromDbType)
.concat(project.config.teamMemberDefaultSystemPermissions.map(serverPermissionDefinitionJsonFromTeamSystemDbType)),
},
};
}

View File

@ -21,8 +21,17 @@ function EditDialog(props: {
domains: DomainConfigJson[],
project: Project,
type: 'update' | 'create',
editIndex?: number,
}) {
} & (
{
type: 'create',
} |
{
type: 'update',
editIndex: number,
defaultDomain: string,
defaultHandlerPath: string,
}
)) {
const domainFormSchema = yup.object({
makeSureAlert: yup.mixed().meta({
stackFormFieldRender: () => (
@ -35,18 +44,18 @@ function EditDialog(props: {
.matches(/^https?:\/\//, "Origin must start with http:// or https://")
.url("Domain must be a valid URL")
.notOneOf(props.domains
.filter((_, i) => i !== props.editIndex)
.filter((_, i) => props.type === 'update' && i !== props.editIndex)
.map(({ domain }) => domain), "Domain already exists")
.required()
.label("Origin (protocol + domain)")
.meta({
stackFormFieldPlaceholder: "https://example.com",
}),
}).default(props.type === 'update' ? props.defaultDomain : ""),
handlerPath: yup.string()
.matches(/^\//, "Handler path must start with /")
.required()
.label("Handler path")
.default("/handler"),
.default(props.type === 'update' ? props.defaultHandlerPath : "/handler"),
});
return <SmartFormDialog
@ -163,6 +172,8 @@ export default function PageClient() {
project={project}
type="update"
editIndex={i}
defaultDomain={domain}
defaultHandlerPath={handlerPath}
/>
<DeleteDialog
open={isDeleteModalOpen}

View File

@ -1,7 +1,7 @@
"use client";
import { useAdminApp } from "../use-admin-app";
import { PageLayout } from "../page-layout";
import { SettingCard, SettingSwitch, SettingText } from "@/components/settings";
import { SettingCard, SettingSwitch } from "@/components/settings";
import Typography from "@/components/ui/typography";
import { SmartFormDialog } from "@/components/form-dialog";
import { PermissionListField } from "@/components/permission-field";
@ -16,22 +16,22 @@ function CreateDialog(props: {
const stackAdminApp = useAdminApp();
const project = stackAdminApp.useProjectAdmin();
const permissions = stackAdminApp.usePermissionDefinitions();
const selectedPermissionIds = props.type === "creator" ?
project.evaluatedConfig.teamCreatorDefaultPermissions.map(x => x.id) :
project.evaluatedConfig.teamMemberDefaultPermissions.map(x => x.id);
const formSchema = yup.object({
permissions: yup.array().of(yup.string().required()).required().default([]).meta({
permissions: yup.array().of(yup.string().required()).required().meta({
stackFormFieldRender: (props) => (
<PermissionListField
{...props}
permissions={permissions}
type="new"
permissions={permissions}
selectedPermissionIds={selectedPermissionIds}
type="select"
label="Default Permissions"
/>
),
}),
}).default({
permissions: props.type === "creator" ?
project.evaluatedConfig.teamCreatorDefaultPermissionIds :
project.evaluatedConfig.teamMemberDefaultPermissionIds
}).default(selectedPermissionIds),
});
return <SmartFormDialog
@ -86,12 +86,12 @@ export default function PageClient() {
type: 'creator',
title: "Team Creator Default Permissions",
description: "Permissions the user will automatically be granted when creating a team",
key: 'teamCreatorDefaultPermissionIds',
key: 'teamCreatorDefaultPermissions',
}, {
type: 'member',
title: "Team Member Default Permissions",
description: "Permissions the user will automatically be granted when joining a team",
key: 'teamMemberDefaultPermissionIds',
key: 'teamMemberDefaultPermissions',
}
] as const).map(({ type, title, description, key }) => (
<SettingCard
@ -105,8 +105,8 @@ export default function PageClient() {
>
<div className="flex flex-wrap gap-2">
{project.evaluatedConfig[key].length > 0 ?
project.evaluatedConfig[key].map((permissionId) => (
<Badge key={permissionId} variant='secondary'>{permissionId}</Badge>
project.evaluatedConfig[key].map((p) => (
<Badge key={p.id} variant='secondary'>{p.id}</Badge>
)) :
<Typography variant="secondary" type="label">No default permissions set</Typography>
}

View File

@ -123,7 +123,7 @@ export function PermissionListField<F extends FieldValues>(props: {
name: Path<F>,
label: React.ReactNode,
permissions: ServerPermissionDefinitionJson[],
type: 'new' | 'edit' | 'edit-user',
type: 'new' | 'edit' | 'edit-user' | 'select',
} & ({
type: 'new',
} | {
@ -133,6 +133,9 @@ export function PermissionListField<F extends FieldValues>(props: {
type: 'edit-user',
user: ServerUser,
team: ServerTeam,
} | {
type: 'select',
selectedPermissionIds: string[],
})) {
const [graph, setGraph] = useState<PermissionGraph>();
@ -155,11 +158,15 @@ export function PermissionListField<F extends FieldValues>(props: {
setGraph(newGraph.addPermission());
break;
}
case 'select': {
setGraph(newGraph.addPermission(props.selectedPermissionIds));
break;
}
}
}
load().catch(console.error);
// @ts-ignore
}, [props.permissions, props.selectedPermissionId, props.type, props.user, props.team]);
}, [props.permissions, props.selectedPermissionId, props.type, props.user, props.team, props.selectedPermissionIds]);
if (!graph || graph.permissions.size <= 1) {
return null;

View File

@ -46,7 +46,7 @@ const teamSystemPermissionDescriptionMap: Record<DBTeamSystemPermission, string>
"INVITE_MEMBERS": "Invite other users to the team",
};
function serverPermissionDefinitionJsonFromDbType(
export function serverPermissionDefinitionJsonFromDbType(
db: Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>
): ServerPermissionDefinitionJson {
if (!db.projectConfigId && !db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId`, { db });
@ -74,7 +74,7 @@ function serverPermissionDefinitionJsonFromDbType(
};
}
function serverPermissionDefinitionJsonFromTeamSystemDbType(
export function serverPermissionDefinitionJsonFromTeamSystemDbType(
db: DBTeamSystemPermission,
): ServerPermissionDefinitionJson {
return {

View File

@ -8,7 +8,7 @@ import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
import { EmailConfigJson, SharedProvider, StandardProvider, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/interface/clientInterface";
import { OAuthProviderUpdateOptions, ProjectUpdateOptions } from "@stackframe/stack-shared/dist/interface/adminInterface";
import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
import { isTeamSystemPermission, listServerPermissionDefinitions, teamDBTypeToSystemPermissionString, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions";
import { fullPermissionInclude, isTeamSystemPermission, listServerPermissionDefinitions, serverPermissionDefinitionJsonFromDbType, serverPermissionDefinitionJsonFromTeamSystemDbType, teamDBTypeToSystemPermissionString, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions";
function toDBSharedProvider(type: SharedProvider): ProxiedOAuthProviderType {
@ -67,7 +67,9 @@ export const fullProjectInclude = {
standardEmailServiceConfig: true,
},
},
permissions: true,
permissions: {
include: fullPermissionInclude,
},
domains: true,
},
},
@ -483,11 +485,13 @@ async function _createDefaultPermissionsUpdateTransactions(
const params = [
{
type: 'creator',
optionName: 'teamCreatorDefaultPermissionIds',
dbName: 'teamCreatorDefaultPermissions',
dbSystemName: 'teamCreateDefaultSystemPermissions',
},
{
type: 'member',
optionName: 'teamMemberDefaultPermissionIds',
dbName: 'teamMemberDefaultPermissions',
dbSystemName: 'teamMemberDefaultSystemPermissions',
@ -500,15 +504,6 @@ async function _createDefaultPermissionsUpdateTransactions(
if (!creatorPerms.every((id) => permissions.some((perm) => perm.id === id))) {
throw new StatusError(StatusError.BadRequest, "Invalid team default permission ids");
}
const connect = creatorPerms
.filter(x => !isTeamSystemPermission(x))
.map((id) => ({
projectConfigId_queryableId: {
projectConfigId: project.config.id,
queryableId: id
},
}));
const systemPerms = creatorPerms
.filter(isTeamSystemPermission)
@ -517,10 +512,36 @@ async function _createDefaultPermissionsUpdateTransactions(
transactions.push(prismaClient.projectConfig.update({
where: { id: project.config.id },
data: {
[param.dbName]: { connect },
[param.dbSystemName]: systemPerms,
},
}));
// Remove existing default permissions
transactions.push(prismaClient.permission.updateMany({
where: {
projectConfigId: project.config.id,
scope: 'TEAM',
},
data: {
isDefaultTeamCreatorPermission: param.type === 'creator' ? false : undefined,
isDefaultTeamMemberPermission: param.type === 'member' ? false : undefined,
},
}));
// Add new default permissions
transactions.push(prismaClient.permission.updateMany({
where: {
projectConfigId: project.config.id,
queryableId: {
in: creatorPerms.filter(x => !isTeamSystemPermission(x)),
},
scope: 'TEAM',
},
data: {
isDefaultTeamCreatorPermission: param.type === 'creator',
isDefaultTeamMemberPermission: param.type === 'member',
},
}));
}
}
@ -660,12 +681,12 @@ export function projectJsonFromDbType(project: ProjectDB): ProjectJson {
return [];
}),
emailConfig,
teamCreatorDefaultPermissionIds: project.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission)
.map((perm) => perm.queryableId)
.concat(project.config.teamCreateDefaultSystemPermissions.map(teamDBTypeToSystemPermissionString)),
teamMemberDefaultPermissionIds: project.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission)
.map((perm) => perm.queryableId)
.concat(project.config.teamMemberDefaultSystemPermissions.map(teamDBTypeToSystemPermissionString)),
teamCreatorDefaultPermissions: project.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission)
.map(serverPermissionDefinitionJsonFromDbType)
.concat(project.config.teamCreateDefaultSystemPermissions.map(serverPermissionDefinitionJsonFromTeamSystemDbType)),
teamMemberDefaultPermissions: project.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission)
.map(serverPermissionDefinitionJsonFromDbType)
.concat(project.config.teamMemberDefaultSystemPermissions.map(serverPermissionDefinitionJsonFromTeamSystemDbType)),
},
};
}

View File

@ -175,8 +175,8 @@ async function grantDefaultTeamPermissions(options: { projectId: string, teamId:
}
const permissionIds = options.type === 'creator' ?
project.evaluatedConfig.teamCreatorDefaultPermissionIds :
project.evaluatedConfig.teamMemberDefaultPermissionIds;
project.evaluatedConfig.teamCreatorDefaultPermissions.map(x => x.id) :
project.evaluatedConfig.teamMemberDefaultPermissions.map(x => x.id);
// TODO: improve performance by batching
for (const permissionId of permissionIds) {

View File

@ -100,8 +100,8 @@ export type ProjectJson = {
emailConfig?: EmailConfigJson,
domains: DomainConfigJson[],
createTeamOnSignUp: boolean,
teamCreatorDefaultPermissionIds: string[],
teamMemberDefaultPermissionIds: string[],
teamCreatorDefaultPermissions: PermissionDefinitionJson[],
teamMemberDefaultPermissions: PermissionDefinitionJson[],
},
};

View File

@ -870,8 +870,8 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
emailConfig: data.evaluatedConfig.emailConfig,
domains: data.evaluatedConfig.domains,
createTeamOnSignUp: data.evaluatedConfig.createTeamOnSignUp,
teamCreatorDefaultPermissionIds: data.evaluatedConfig.teamCreatorDefaultPermissionIds,
teamMemberDefaultPermissionIds: data.evaluatedConfig.teamMemberDefaultPermissionIds,
teamCreatorDefaultPermissions: data.evaluatedConfig.teamCreatorDefaultPermissions,
teamMemberDefaultPermissions: data.evaluatedConfig.teamMemberDefaultPermissions,
},
async update(update: ProjectUpdateOptions) {
@ -2026,8 +2026,8 @@ export type Project = {
readonly emailConfig?: EmailConfig,
readonly domains: DomainConfig[],
readonly createTeamOnSignUp: boolean,
readonly teamCreatorDefaultPermissionIds: string[],
readonly teamMemberDefaultPermissionIds: string[],
readonly teamCreatorDefaultPermissions: PermissionDefinitionJson[],
readonly teamMemberDefaultPermissions: PermissionDefinitionJson[],
},
update(this: Project, update: ProjectUpdateOptions): Promise<void>,