diff --git a/packages/stack-server/prisma/migrations/20240520152704_selected_team/migration.sql b/packages/stack-server/prisma/migrations/20240520152704_selected_team/migration.sql new file mode 100644 index 000000000..9d7a06a31 --- /dev/null +++ b/packages/stack-server/prisma/migrations/20240520152704_selected_team/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "ProjectUser" ADD COLUMN "selectedTeamId" UUID; + +-- AddForeignKey +ALTER TABLE "ProjectUser" ADD CONSTRAINT "ProjectUser_projectId_selectedTeamId_fkey" FOREIGN KEY ("projectId", "selectedTeamId") REFERENCES "Team"("projectId", "teamId") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/packages/stack-server/prisma/schema.prisma b/packages/stack-server/prisma/schema.prisma index bbd07be5a..145b5f133 100644 --- a/packages/stack-server/prisma/schema.prisma +++ b/packages/stack-server/prisma/schema.prisma @@ -90,9 +90,10 @@ model Team { displayName String - project Project @relation(fields: [projectId], references: [id]) - permissions Permission[] - teamMembers TeamMember[] + project Project @relation(fields: [projectId], references: [id]) + permissions Permission[] + teamMembers TeamMember[] + selectedProjectUser ProjectUser[] @@id([projectId, teamId]) } @@ -199,6 +200,9 @@ model ProjectUser { serverMetadata Json? clientMetadata Json? + selectedTeam Team? @relation(fields: [projectId, selectedTeamId], references: [projectId, teamId]) + selectedTeamId String? @db.Uuid + @@id([projectId, projectUserId]) } diff --git a/packages/stack-server/src/app/api/v1/users/crud.tsx b/packages/stack-server/src/app/api/v1/users/crud.tsx index b91325365..4dc45c840 100644 --- a/packages/stack-server/src/app/api/v1/users/crud.tsx +++ b/packages/stack-server/src/app/api/v1/users/crud.tsx @@ -52,6 +52,7 @@ export const usersCrudHandlers = createPrismaCrudHandlers(usersCrud, "projectUse hasPassword: !!prisma.passwordHash, authWithEmail: prisma.authWithEmail, oauthProviders: prisma.projectUserOAuthAccounts.map((a) => a.oauthProviderConfigId), + selectedTeamId: prisma.selectedTeamId, }; }, }); diff --git a/packages/stack-server/src/lib/users.tsx b/packages/stack-server/src/lib/users.tsx index 597ba4572..97793dc44 100644 --- a/packages/stack-server/src/lib/users.tsx +++ b/packages/stack-server/src/lib/users.tsx @@ -43,6 +43,7 @@ export async function updateClientUser( { displayName: update.displayName, clientMetadata: update.clientMetadata, + selectedTeamId: update.selectedTeamId, }, ); if (!user) { @@ -76,6 +77,7 @@ export async function updateServerUser( primaryEmailVerified: update.primaryEmailVerified, clientMetadata: update.clientMetadata as any, serverMetadata: update.serverMetadata as any, + selectedTeamId: update.selectedTeamId, }), }); } catch (e) { @@ -122,6 +124,7 @@ function getClientUserFromServerUser(serverUser: ServerUserJson): UserJson { authWithEmail: serverUser.authWithEmail, hasPassword: serverUser.hasPassword, oauthProviders: serverUser.oauthProviders, + selectedTeamId: serverUser.selectedTeamId, }; } @@ -142,6 +145,7 @@ export function getServerUserFromDbType( hasPassword: !!user.passwordHash, authWithEmail: user.authWithEmail, oauthProviders: user.projectUserOAuthAccounts.map((a) => a.oauthProviderConfigId), + selectedTeamId: user.selectedTeamId, }; } diff --git a/packages/stack-shared/src/interface/clientInterface.ts b/packages/stack-shared/src/interface/clientInterface.ts index 3936577e5..e79b3aa66 100644 --- a/packages/stack-shared/src/interface/clientInterface.ts +++ b/packages/stack-shared/src/interface/clientInterface.ts @@ -13,6 +13,7 @@ import { generateSecureRandomString } from '../utils/crypto'; type UserCustomizableJson = { displayName: string | null, clientMetadata: ReadonlyJson, + selectedTeamId: string | null, }; export type UserJson = UserCustomizableJson & { @@ -31,6 +32,7 @@ export type UserJson = UserCustomizableJson & { hasPassword: boolean, authWithEmail: boolean, oauthProviders: string[], + selectedTeamId: string | null, }; export type UserUpdateJson = Partial; diff --git a/packages/stack-shared/src/interface/crud/current-user.ts b/packages/stack-shared/src/interface/crud/current-user.ts index da5b77a8e..df83aa1bc 100644 --- a/packages/stack-shared/src/interface/crud/current-user.ts +++ b/packages/stack-shared/src/interface/crud/current-user.ts @@ -5,6 +5,7 @@ import { usersCrudServerReadSchema, usersCrudServerUpdateSchema } from "./users" const clientUpdateSchema = usersCrudServerUpdateSchema.pick([ "displayName", "clientMetadata", + "selectedTeamId", ]).required(); const serverUpdateSchema = usersCrudServerUpdateSchema; @@ -22,6 +23,7 @@ const clientReadSchema = usersCrudServerReadSchema.pick([ "hasPassword", "authWithEmail", "oauthProviders", + "selectedTeamId", ]).nullable().defined(); const serverReadSchema = usersCrudServerReadSchema.nullable().defined(); diff --git a/packages/stack-shared/src/interface/crud/users.ts b/packages/stack-shared/src/interface/crud/users.ts index 65b449e24..db5e2d636 100644 --- a/packages/stack-shared/src/interface/crud/users.ts +++ b/packages/stack-shared/src/interface/crud/users.ts @@ -7,6 +7,7 @@ export const usersCrudServerUpdateSchema = yup.object({ serverMetadata: yup.object().optional(), primaryEmail: yup.string().optional(), primaryEmailVerified: yup.boolean().optional(), + selectedTeamId: yup.string().nullable().optional(), }).required(); export const usersCrudServerReadSchema = yup.object({ @@ -16,6 +17,7 @@ export const usersCrudServerReadSchema = yup.object({ primaryEmailVerified: yup.boolean().required(), displayName: yup.string().nullable().defined(), clientMetadata: yup.object().nullable().defined().transform((value) => JSON.parse(JSON.stringify(value))), + selectedTeamId: yup.string().nullable().defined(), profileImageUrl: yup.string().nullable().defined(), signedUpAtMillis: yup.number().required(), /** diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 03e863375..5bf46a0e7 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -387,6 +387,12 @@ class _StackClientAppImpl t.id === teamId) ?? null; @@ -461,6 +467,9 @@ class _StackClientAppImpl = { readonly tokenStore: ReadonlyTokenStore, + updateSelectedTeam(this: T, team: Team | null): Promise, update(this: T, user: C): Promise, signOut(this: T): Promise, sendVerificationEmail(this: T): Promise, @@ -1461,6 +1474,8 @@ export type User = ( readonly oauthProviders: readonly string[], hasPermission(this: CurrentUser, scope: Team, permissionId: string): Promise, + getSelectedTeam(this: CurrentUser): Promise, + useSelectedTeam(this: CurrentUser): Team | null, toJson(this: CurrentUser): UserJson, }