diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index a385b4f42..7e66f2fb1 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -222,11 +222,15 @@ model ProjectUser { projectUserRefreshTokens ProjectUserRefreshToken[] projectUserAuthorizationCodes ProjectUserAuthorizationCode[] projectUserOAuthAccounts ProjectUserOAuthAccount[] - projectUserEmailVerificationCode ProjectUserEmailVerificationCode[] - projectUserPasswordResetCode ProjectUserPasswordResetCode[] - projectUserMagicLinkCode ProjectUserMagicLinkCode[] teamMembers TeamMember[] + // @deprecated + projectUserEmailVerificationCode ProjectUserEmailVerificationCode[] + // @deprecated + projectUserPasswordResetCode ProjectUserPasswordResetCode[] + // @deprecated + projectUserMagicLinkCode ProjectUserMagicLinkCode[] + primaryEmail String? primaryEmailVerified Boolean profileImageUrl String? diff --git a/apps/backend/src/app/api/v1/users/crud.tsx b/apps/backend/src/app/api/v1/users/crud.tsx index 368707c89..fc15031ff 100644 --- a/apps/backend/src/app/api/v1/users/crud.tsx +++ b/apps/backend/src/app/api/v1/users/crud.tsx @@ -3,16 +3,20 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { BooleanTrue, Prisma } from "@prisma/client"; import { KnownErrors } from "@stackframe/stack-shared"; import { currentUserCrud } from "@stackframe/stack-shared/dist/interface/crud/current-user"; -import { usersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; +import { UsersCrud, usersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { userIdOrMeSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { hashPassword } from "@stackframe/stack-shared/dist/utils/password"; import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; import { teamPrismaToCrud } from "../teams/crud"; import { sendUserCreatedWebhook, sendUserDeletedWebhook, sendUserUpdatedWebhook } from "@/lib/webhooks"; const fullInclude = { - projectUserOAuthAccounts: true, + projectUserOAuthAccounts: { + include: { + providerConfig: true, + }, + }, teamMembers: { include: { team: true, @@ -21,13 +25,51 @@ const fullInclude = { isSelected: BooleanTrue.TRUE, }, }, -}; +} satisfies Prisma.ProjectUserInclude; -const prismaToCrud = (prisma: Prisma.ProjectUserGetPayload<{ include: typeof fullInclude}>) => { +const prismaToCrud = (prisma: Prisma.ProjectUserGetPayload<{ include: typeof fullInclude}>): UsersCrud["Admin"]["Read"] => { const selectedTeamMembers = prisma.teamMembers; if (selectedTeamMembers.length > 1) { throw new StackAssertionError("User cannot have more than one selected team; this should never happen"); } + + if (prisma.passwordHash && !prisma.authWithEmail) { + captureError("prismaToCrud", new StackAssertionError("User has password but authWithEmail is false; this is an assertion error that should never happen", { prisma })); + } + if (prisma.authWithEmail && !prisma.primaryEmail) { + captureError("prismaToCrud", new StackAssertionError("User has authWithEmail but no primary email; this is an assertion error that should never happen", { prisma })); + } + const authMethods: UsersCrud["Admin"]["Read"]["auth_methods"] = [ + ...prisma.passwordHash ? [{ + type: 'password', + identifier: prisma.primaryEmail ?? "", + }] as const : [], + ...prisma.authWithEmail ? [{ + type: 'otp', + contact_channel: { + type: 'email', + email: prisma.primaryEmail ?? "", + }, + }] as const : [], + ...prisma.projectUserOAuthAccounts.map((a) => ({ + type: 'oauth', + provider: { + type: a.oauthProviderConfigId, + provider_user_id: a.providerAccountId, + }, + } as const)), + ] as const; + + const connectedAccounts: UsersCrud["Admin"]["Read"]["connected_accounts"] = [ + ...prisma.projectUserOAuthAccounts.map((a) => ({ + type: 'oauth', + provider: { + type: a.oauthProviderConfigId, + provider_user_id: a.providerAccountId, + }, + } as const)), + ]; + return { id: prisma.projectUserId, display_name: prisma.displayName || null, @@ -37,7 +79,6 @@ const prismaToCrud = (prisma: Prisma.ProjectUserGetPayload<{ include: typeof ful signed_up_at_millis: prisma.createdAt.getTime(), client_metadata: prisma.clientMetadata, server_metadata: prisma.serverMetadata, - auth_method: prisma.passwordHash ? 'credential' as const : 'oauth' as const, // not used anymore, for backwards compatibility has_password: !!prisma.passwordHash, auth_with_email: prisma.authWithEmail, oauth_providers: prisma.projectUserOAuthAccounts.map((a) => ({ @@ -45,6 +86,8 @@ const prismaToCrud = (prisma: Prisma.ProjectUserGetPayload<{ include: typeof ful account_id: a.providerAccountId, email: a.email, })), + auth_methods: authMethods, + connected_accounts: connectedAccounts, selected_team_id: selectedTeamMembers[0]?.teamId ?? null, selected_team: selectedTeamMembers[0] ? teamPrismaToCrud(selectedTeamMembers[0]?.team) : null, }; diff --git a/apps/dashboard/prisma/schema.prisma b/apps/dashboard/prisma/schema.prisma index a385b4f42..7e66f2fb1 100644 --- a/apps/dashboard/prisma/schema.prisma +++ b/apps/dashboard/prisma/schema.prisma @@ -222,11 +222,15 @@ model ProjectUser { projectUserRefreshTokens ProjectUserRefreshToken[] projectUserAuthorizationCodes ProjectUserAuthorizationCode[] projectUserOAuthAccounts ProjectUserOAuthAccount[] - projectUserEmailVerificationCode ProjectUserEmailVerificationCode[] - projectUserPasswordResetCode ProjectUserPasswordResetCode[] - projectUserMagicLinkCode ProjectUserMagicLinkCode[] teamMembers TeamMember[] + // @deprecated + projectUserEmailVerificationCode ProjectUserEmailVerificationCode[] + // @deprecated + projectUserPasswordResetCode ProjectUserPasswordResetCode[] + // @deprecated + projectUserMagicLinkCode ProjectUserMagicLinkCode[] + primaryEmail String? primaryEmailVerified Boolean profileImageUrl String? diff --git a/apps/e2e/tests/backend/backend-helpers.ts b/apps/e2e/tests/backend/backend-helpers.ts index bd39d9ea0..d58946a82 100644 --- a/apps/e2e/tests/backend/backend-helpers.ts +++ b/apps/e2e/tests/backend/backend-helpers.ts @@ -115,6 +115,7 @@ export namespace Auth { headers: expect.anything(), body: expect.anything(), }); + return response; } export async function expectToBeSignedOut() { diff --git a/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-in.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-in.test.ts index f78a43045..36806bf41 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-in.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-in.test.ts @@ -17,7 +17,22 @@ it("should allow signing in to existing accounts", async ({ expect }) => { "headers": Headers {