Skip to content

Commit bd96da6

Browse files
authored
Fix client side team bugs (stack-auth#86)
* permission ids -> definition jsons * fixed default permission update bug * fixed set team default permission * fixed handler edit dialog
1 parent 3cd4d3f commit bd96da6

File tree

10 files changed

+99
-58
lines changed

10 files changed

+99
-58
lines changed

apps/backend/src/lib/permissions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const teamSystemPermissionDescriptionMap: Record<DBTeamSystemPermission, string>
4646
"INVITE_MEMBERS": "Invite other users to the team",
4747
};
4848

49-
function serverPermissionDefinitionJsonFromDbType(
49+
export function serverPermissionDefinitionJsonFromDbType(
5050
db: Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>
5151
): ServerPermissionDefinitionJson {
5252
if (!db.projectConfigId && !db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId`, { db });
@@ -74,7 +74,7 @@ function serverPermissionDefinitionJsonFromDbType(
7474
};
7575
}
7676

77-
function serverPermissionDefinitionJsonFromTeamSystemDbType(
77+
export function serverPermissionDefinitionJsonFromTeamSystemDbType(
7878
db: DBTeamSystemPermission,
7979
): ServerPermissionDefinitionJson {
8080
return {

apps/backend/src/lib/projects.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
88
import { EmailConfigJson, SharedProvider, StandardProvider, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/interface/clientInterface";
99
import { OAuthProviderUpdateOptions, ProjectUpdateOptions } from "@stackframe/stack-shared/dist/interface/adminInterface";
1010
import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
11-
import { isTeamSystemPermission, listServerPermissionDefinitions, teamDBTypeToSystemPermissionString, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions";
11+
import { fullPermissionInclude, isTeamSystemPermission, listServerPermissionDefinitions, serverPermissionDefinitionJsonFromDbType, serverPermissionDefinitionJsonFromTeamSystemDbType, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions";
1212

1313

1414
function toDBSharedProvider(type: SharedProvider): ProxiedOAuthProviderType {
@@ -67,7 +67,9 @@ export const fullProjectInclude = {
6767
standardEmailServiceConfig: true,
6868
},
6969
},
70-
permissions: true,
70+
permissions: {
71+
include: fullPermissionInclude,
72+
},
7173
domains: true,
7274
},
7375
},
@@ -660,12 +662,12 @@ export function projectJsonFromDbType(project: ProjectDB): ProjectJson {
660662
return [];
661663
}),
662664
emailConfig,
663-
teamCreatorDefaultPermissionIds: project.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission)
664-
.map((perm) => perm.queryableId)
665-
.concat(project.config.teamCreateDefaultSystemPermissions.map(teamDBTypeToSystemPermissionString)),
666-
teamMemberDefaultPermissionIds: project.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission)
667-
.map((perm) => perm.queryableId)
668-
.concat(project.config.teamMemberDefaultSystemPermissions.map(teamDBTypeToSystemPermissionString)),
665+
teamCreatorDefaultPermissions: project.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission)
666+
.map(serverPermissionDefinitionJsonFromDbType)
667+
.concat(project.config.teamCreateDefaultSystemPermissions.map(serverPermissionDefinitionJsonFromTeamSystemDbType)),
668+
teamMemberDefaultPermissions: project.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission)
669+
.map(serverPermissionDefinitionJsonFromDbType)
670+
.concat(project.config.teamMemberDefaultSystemPermissions.map(serverPermissionDefinitionJsonFromTeamSystemDbType)),
669671
},
670672
};
671673
}

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/domains/page-client.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,17 @@ function EditDialog(props: {
2121
domains: DomainConfigJson[],
2222
project: Project,
2323
type: 'update' | 'create',
24-
editIndex?: number,
25-
}) {
24+
} & (
25+
{
26+
type: 'create',
27+
} |
28+
{
29+
type: 'update',
30+
editIndex: number,
31+
defaultDomain: string,
32+
defaultHandlerPath: string,
33+
}
34+
)) {
2635
const domainFormSchema = yup.object({
2736
makeSureAlert: yup.mixed().meta({
2837
stackFormFieldRender: () => (
@@ -35,18 +44,18 @@ function EditDialog(props: {
3544
.matches(/^https?:\/\//, "Origin must start with http:// or https://")
3645
.url("Domain must be a valid URL")
3746
.notOneOf(props.domains
38-
.filter((_, i) => i !== props.editIndex)
47+
.filter((_, i) => props.type === 'update' && i !== props.editIndex)
3948
.map(({ domain }) => domain), "Domain already exists")
4049
.required()
4150
.label("Origin (protocol + domain)")
4251
.meta({
4352
stackFormFieldPlaceholder: "https://example.com",
44-
}),
53+
}).default(props.type === 'update' ? props.defaultDomain : ""),
4554
handlerPath: yup.string()
4655
.matches(/^\//, "Handler path must start with /")
4756
.required()
4857
.label("Handler path")
49-
.default("/handler"),
58+
.default(props.type === 'update' ? props.defaultHandlerPath : "/handler"),
5059
});
5160

5261
return <SmartFormDialog
@@ -163,6 +172,8 @@ export default function PageClient() {
163172
project={project}
164173
type="update"
165174
editIndex={i}
175+
defaultDomain={domain}
176+
defaultHandlerPath={handlerPath}
166177
/>
167178
<DeleteDialog
168179
open={isDeleteModalOpen}

apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/team-settings/page-client.tsx

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22
import { useAdminApp } from "../use-admin-app";
33
import { PageLayout } from "../page-layout";
4-
import { SettingCard, SettingSwitch, SettingText } from "@/components/settings";
4+
import { SettingCard, SettingSwitch } from "@/components/settings";
55
import Typography from "@/components/ui/typography";
66
import { SmartFormDialog } from "@/components/form-dialog";
77
import { PermissionListField } from "@/components/permission-field";
@@ -16,22 +16,22 @@ function CreateDialog(props: {
1616
const stackAdminApp = useAdminApp();
1717
const project = stackAdminApp.useProjectAdmin();
1818
const permissions = stackAdminApp.usePermissionDefinitions();
19+
const selectedPermissionIds = props.type === "creator" ?
20+
project.evaluatedConfig.teamCreatorDefaultPermissions.map(x => x.id) :
21+
project.evaluatedConfig.teamMemberDefaultPermissions.map(x => x.id);
1922

2023
const formSchema = yup.object({
21-
permissions: yup.array().of(yup.string().required()).required().default([]).meta({
24+
permissions: yup.array().of(yup.string().required()).required().meta({
2225
stackFormFieldRender: (props) => (
2326
<PermissionListField
2427
{...props}
25-
permissions={permissions}
26-
type="new"
28+
permissions={permissions}
29+
selectedPermissionIds={selectedPermissionIds}
30+
type="select"
2731
label="Default Permissions"
2832
/>
2933
),
30-
}),
31-
}).default({
32-
permissions: props.type === "creator" ?
33-
project.evaluatedConfig.teamCreatorDefaultPermissionIds :
34-
project.evaluatedConfig.teamMemberDefaultPermissionIds
34+
}).default(selectedPermissionIds),
3535
});
3636

3737
return <SmartFormDialog
@@ -86,12 +86,12 @@ export default function PageClient() {
8686
type: 'creator',
8787
title: "Team Creator Default Permissions",
8888
description: "Permissions the user will automatically be granted when creating a team",
89-
key: 'teamCreatorDefaultPermissionIds',
89+
key: 'teamCreatorDefaultPermissions',
9090
}, {
9191
type: 'member',
9292
title: "Team Member Default Permissions",
9393
description: "Permissions the user will automatically be granted when joining a team",
94-
key: 'teamMemberDefaultPermissionIds',
94+
key: 'teamMemberDefaultPermissions',
9595
}
9696
] as const).map(({ type, title, description, key }) => (
9797
<SettingCard
@@ -105,8 +105,8 @@ export default function PageClient() {
105105
>
106106
<div className="flex flex-wrap gap-2">
107107
{project.evaluatedConfig[key].length > 0 ?
108-
project.evaluatedConfig[key].map((permissionId) => (
109-
<Badge key={permissionId} variant='secondary'>{permissionId}</Badge>
108+
project.evaluatedConfig[key].map((p) => (
109+
<Badge key={p.id} variant='secondary'>{p.id}</Badge>
110110
)) :
111111
<Typography variant="secondary" type="label">No default permissions set</Typography>
112112
}

apps/dashboard/src/components/permission-field.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ export function PermissionListField<F extends FieldValues>(props: {
123123
name: Path<F>,
124124
label: React.ReactNode,
125125
permissions: ServerPermissionDefinitionJson[],
126-
type: 'new' | 'edit' | 'edit-user',
126+
type: 'new' | 'edit' | 'edit-user' | 'select',
127127
} & ({
128128
type: 'new',
129129
} | {
@@ -133,6 +133,9 @@ export function PermissionListField<F extends FieldValues>(props: {
133133
type: 'edit-user',
134134
user: ServerUser,
135135
team: ServerTeam,
136+
} | {
137+
type: 'select',
138+
selectedPermissionIds: string[],
136139
})) {
137140
const [graph, setGraph] = useState<PermissionGraph>();
138141

@@ -155,11 +158,15 @@ export function PermissionListField<F extends FieldValues>(props: {
155158
setGraph(newGraph.addPermission());
156159
break;
157160
}
161+
case 'select': {
162+
setGraph(newGraph.addPermission(props.selectedPermissionIds));
163+
break;
164+
}
158165
}
159166
}
160167
load().catch(console.error);
161168
// @ts-ignore
162-
}, [props.permissions, props.selectedPermissionId, props.type, props.user, props.team]);
169+
}, [props.permissions, props.selectedPermissionId, props.type, props.user, props.team, props.selectedPermissionIds]);
163170

164171
if (!graph || graph.permissions.size <= 1) {
165172
return null;

apps/dashboard/src/lib/permissions.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const teamSystemPermissionDescriptionMap: Record<DBTeamSystemPermission, string>
4646
"INVITE_MEMBERS": "Invite other users to the team",
4747
};
4848

49-
function serverPermissionDefinitionJsonFromDbType(
49+
export function serverPermissionDefinitionJsonFromDbType(
5050
db: Prisma.PermissionGetPayload<{ include: typeof fullPermissionInclude }>
5151
): ServerPermissionDefinitionJson {
5252
if (!db.projectConfigId && !db.teamId) throw new StackAssertionError(`Permission DB object should have either projectConfigId or teamId`, { db });
@@ -74,7 +74,7 @@ function serverPermissionDefinitionJsonFromDbType(
7474
};
7575
}
7676

77-
function serverPermissionDefinitionJsonFromTeamSystemDbType(
77+
export function serverPermissionDefinitionJsonFromTeamSystemDbType(
7878
db: DBTeamSystemPermission,
7979
): ServerPermissionDefinitionJson {
8080
return {

apps/dashboard/src/lib/projects.tsx

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
88
import { EmailConfigJson, SharedProvider, StandardProvider, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/interface/clientInterface";
99
import { OAuthProviderUpdateOptions, ProjectUpdateOptions } from "@stackframe/stack-shared/dist/interface/adminInterface";
1010
import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors";
11-
import { isTeamSystemPermission, listServerPermissionDefinitions, teamDBTypeToSystemPermissionString, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions";
11+
import { fullPermissionInclude, isTeamSystemPermission, listServerPermissionDefinitions, serverPermissionDefinitionJsonFromDbType, serverPermissionDefinitionJsonFromTeamSystemDbType, teamDBTypeToSystemPermissionString, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions";
1212

1313

1414
function toDBSharedProvider(type: SharedProvider): ProxiedOAuthProviderType {
@@ -67,7 +67,9 @@ export const fullProjectInclude = {
6767
standardEmailServiceConfig: true,
6868
},
6969
},
70-
permissions: true,
70+
permissions: {
71+
include: fullPermissionInclude,
72+
},
7173
domains: true,
7274
},
7375
},
@@ -483,11 +485,13 @@ async function _createDefaultPermissionsUpdateTransactions(
483485

484486
const params = [
485487
{
488+
type: 'creator',
486489
optionName: 'teamCreatorDefaultPermissionIds',
487490
dbName: 'teamCreatorDefaultPermissions',
488491
dbSystemName: 'teamCreateDefaultSystemPermissions',
489492
},
490493
{
494+
type: 'member',
491495
optionName: 'teamMemberDefaultPermissionIds',
492496
dbName: 'teamMemberDefaultPermissions',
493497
dbSystemName: 'teamMemberDefaultSystemPermissions',
@@ -500,15 +504,6 @@ async function _createDefaultPermissionsUpdateTransactions(
500504
if (!creatorPerms.every((id) => permissions.some((perm) => perm.id === id))) {
501505
throw new StatusError(StatusError.BadRequest, "Invalid team default permission ids");
502506
}
503-
504-
const connect = creatorPerms
505-
.filter(x => !isTeamSystemPermission(x))
506-
.map((id) => ({
507-
projectConfigId_queryableId: {
508-
projectConfigId: project.config.id,
509-
queryableId: id
510-
},
511-
}));
512507

513508
const systemPerms = creatorPerms
514509
.filter(isTeamSystemPermission)
@@ -517,10 +512,36 @@ async function _createDefaultPermissionsUpdateTransactions(
517512
transactions.push(prismaClient.projectConfig.update({
518513
where: { id: project.config.id },
519514
data: {
520-
[param.dbName]: { connect },
521515
[param.dbSystemName]: systemPerms,
522516
},
523517
}));
518+
519+
// Remove existing default permissions
520+
transactions.push(prismaClient.permission.updateMany({
521+
where: {
522+
projectConfigId: project.config.id,
523+
scope: 'TEAM',
524+
},
525+
data: {
526+
isDefaultTeamCreatorPermission: param.type === 'creator' ? false : undefined,
527+
isDefaultTeamMemberPermission: param.type === 'member' ? false : undefined,
528+
},
529+
}));
530+
531+
// Add new default permissions
532+
transactions.push(prismaClient.permission.updateMany({
533+
where: {
534+
projectConfigId: project.config.id,
535+
queryableId: {
536+
in: creatorPerms.filter(x => !isTeamSystemPermission(x)),
537+
},
538+
scope: 'TEAM',
539+
},
540+
data: {
541+
isDefaultTeamCreatorPermission: param.type === 'creator',
542+
isDefaultTeamMemberPermission: param.type === 'member',
543+
},
544+
}));
524545
}
525546
}
526547

@@ -660,12 +681,12 @@ export function projectJsonFromDbType(project: ProjectDB): ProjectJson {
660681
return [];
661682
}),
662683
emailConfig,
663-
teamCreatorDefaultPermissionIds: project.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission)
664-
.map((perm) => perm.queryableId)
665-
.concat(project.config.teamCreateDefaultSystemPermissions.map(teamDBTypeToSystemPermissionString)),
666-
teamMemberDefaultPermissionIds: project.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission)
667-
.map((perm) => perm.queryableId)
668-
.concat(project.config.teamMemberDefaultSystemPermissions.map(teamDBTypeToSystemPermissionString)),
684+
teamCreatorDefaultPermissions: project.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission)
685+
.map(serverPermissionDefinitionJsonFromDbType)
686+
.concat(project.config.teamCreateDefaultSystemPermissions.map(serverPermissionDefinitionJsonFromTeamSystemDbType)),
687+
teamMemberDefaultPermissions: project.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission)
688+
.map(serverPermissionDefinitionJsonFromDbType)
689+
.concat(project.config.teamMemberDefaultSystemPermissions.map(serverPermissionDefinitionJsonFromTeamSystemDbType)),
669690
},
670691
};
671692
}

apps/dashboard/src/lib/teams.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,8 @@ async function grantDefaultTeamPermissions(options: { projectId: string, teamId:
175175
}
176176

177177
const permissionIds = options.type === 'creator' ?
178-
project.evaluatedConfig.teamCreatorDefaultPermissionIds :
179-
project.evaluatedConfig.teamMemberDefaultPermissionIds;
178+
project.evaluatedConfig.teamCreatorDefaultPermissions.map(x => x.id) :
179+
project.evaluatedConfig.teamMemberDefaultPermissions.map(x => x.id);
180180

181181
// TODO: improve performance by batching
182182
for (const permissionId of permissionIds) {

packages/stack-shared/src/interface/clientInterface.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,8 @@ export type ProjectJson = {
100100
emailConfig?: EmailConfigJson,
101101
domains: DomainConfigJson[],
102102
createTeamOnSignUp: boolean,
103-
teamCreatorDefaultPermissionIds: string[],
104-
teamMemberDefaultPermissionIds: string[],
103+
teamCreatorDefaultPermissions: PermissionDefinitionJson[],
104+
teamMemberDefaultPermissions: PermissionDefinitionJson[],
105105
},
106106
};
107107

packages/stack/src/lib/stack-app.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -870,8 +870,8 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
870870
emailConfig: data.evaluatedConfig.emailConfig,
871871
domains: data.evaluatedConfig.domains,
872872
createTeamOnSignUp: data.evaluatedConfig.createTeamOnSignUp,
873-
teamCreatorDefaultPermissionIds: data.evaluatedConfig.teamCreatorDefaultPermissionIds,
874-
teamMemberDefaultPermissionIds: data.evaluatedConfig.teamMemberDefaultPermissionIds,
873+
teamCreatorDefaultPermissions: data.evaluatedConfig.teamCreatorDefaultPermissions,
874+
teamMemberDefaultPermissions: data.evaluatedConfig.teamMemberDefaultPermissions,
875875
},
876876

877877
async update(update: ProjectUpdateOptions) {
@@ -2026,8 +2026,8 @@ export type Project = {
20262026
readonly emailConfig?: EmailConfig,
20272027
readonly domains: DomainConfig[],
20282028
readonly createTeamOnSignUp: boolean,
2029-
readonly teamCreatorDefaultPermissionIds: string[],
2030-
readonly teamMemberDefaultPermissionIds: string[],
2029+
readonly teamCreatorDefaultPermissions: PermissionDefinitionJson[],
2030+
readonly teamMemberDefaultPermissions: PermissionDefinitionJson[],
20312031
},
20322032

20332033
update(this: Project, update: ProjectUpdateOptions): Promise<void>,

0 commit comments

Comments
 (0)