diff --git a/apps/backend/src/lib/permissions.tsx b/apps/backend/src/lib/permissions.tsx index 04a7789dd..53da68d06 100644 --- a/apps/backend/src/lib/permissions.tsx +++ b/apps/backend/src/lib/permissions.tsx @@ -46,7 +46,7 @@ const teamSystemPermissionDescriptionMap: Record "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 { diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index 9754f1c58..095e5c347 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -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)), }, }; } diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx index 1fd27488d..59eb3b87c 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx @@ -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 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) => ( ), - }), - }).default({ - permissions: props.type === "creator" ? - project.evaluatedConfig.teamCreatorDefaultPermissionIds : - project.evaluatedConfig.teamMemberDefaultPermissionIds + }).default(selectedPermissionIds), }); return (
{project.evaluatedConfig[key].length > 0 ? - project.evaluatedConfig[key].map((permissionId) => ( - {permissionId} + project.evaluatedConfig[key].map((p) => ( + {p.id} )) : No default permissions set } diff --git a/apps/dashboard/src/components/permission-field.tsx b/apps/dashboard/src/components/permission-field.tsx index 9f3bc68e3..2209477e6 100644 --- a/apps/dashboard/src/components/permission-field.tsx +++ b/apps/dashboard/src/components/permission-field.tsx @@ -123,7 +123,7 @@ export function PermissionListField(props: { name: Path, 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(props: { type: 'edit-user', user: ServerUser, team: ServerTeam, + } | { + type: 'select', + selectedPermissionIds: string[], })) { const [graph, setGraph] = useState(); @@ -155,11 +158,15 @@ export function PermissionListField(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; diff --git a/apps/dashboard/src/lib/permissions.tsx b/apps/dashboard/src/lib/permissions.tsx index 04a7789dd..53da68d06 100644 --- a/apps/dashboard/src/lib/permissions.tsx +++ b/apps/dashboard/src/lib/permissions.tsx @@ -46,7 +46,7 @@ const teamSystemPermissionDescriptionMap: Record "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 { diff --git a/apps/dashboard/src/lib/projects.tsx b/apps/dashboard/src/lib/projects.tsx index 9754f1c58..bd0208902 100644 --- a/apps/dashboard/src/lib/projects.tsx +++ b/apps/dashboard/src/lib/projects.tsx @@ -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)), }, }; } diff --git a/apps/dashboard/src/lib/teams.tsx b/apps/dashboard/src/lib/teams.tsx index 520ea05f9..8d60fcebd 100644 --- a/apps/dashboard/src/lib/teams.tsx +++ b/apps/dashboard/src/lib/teams.tsx @@ -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) { diff --git a/packages/stack-shared/src/interface/clientInterface.ts b/packages/stack-shared/src/interface/clientInterface.ts index 7421d141c..27afb1968 100644 --- a/packages/stack-shared/src/interface/clientInterface.ts +++ b/packages/stack-shared/src/interface/clientInterface.ts @@ -100,8 +100,8 @@ export type ProjectJson = { emailConfig?: EmailConfigJson, domains: DomainConfigJson[], createTeamOnSignUp: boolean, - teamCreatorDefaultPermissionIds: string[], - teamMemberDefaultPermissionIds: string[], + teamCreatorDefaultPermissions: PermissionDefinitionJson[], + teamMemberDefaultPermissions: PermissionDefinitionJson[], }, }; diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 0f1a03256..85d386529 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -870,8 +870,8 @@ class _StackClientAppImpl,