diff --git a/apps/backend/src/app/api/latest/auth/password/sign-up/route.tsx b/apps/backend/src/app/api/latest/auth/password/sign-up/route.tsx index c3c5abc2f..ca954beb9 100644 --- a/apps/backend/src/app/api/latest/auth/password/sign-up/route.tsx +++ b/apps/backend/src/app/api/latest/auth/password/sign-up/route.tsx @@ -1,3 +1,4 @@ +import { validateRedirectUrl } from "@/lib/redirect-urls"; import { createAuthTokens } from "@/lib/tokens"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { runAsynchronouslyAndWaitUntil } from "@/utils/vercel"; @@ -39,6 +40,14 @@ export const POST = createSmartRouteHandler({ throw new KnownErrors.PasswordAuthenticationNotEnabled(); } + if (!validateRedirectUrl( + verificationCallbackUrl, + tenancy.config.domains, + tenancy.config.allow_localhost, + )) { + throw new KnownErrors.RedirectUrlNotWhitelisted(); + } + const passwordError = getPasswordError(password); if (passwordError) { throw passwordError; diff --git a/apps/backend/src/app/api/latest/contact-channels/verify/verification-code-handler.tsx b/apps/backend/src/app/api/latest/contact-channels/verify/verification-code-handler.tsx index 3a262a20a..7a59a9978 100644 --- a/apps/backend/src/app/api/latest/contact-channels/verify/verification-code-handler.tsx +++ b/apps/backend/src/app/api/latest/contact-channels/verify/verification-code-handler.tsx @@ -5,6 +5,7 @@ import { createVerificationCodeHandler } from "@/route-handlers/verification-cod import { VerificationCodeType } from "@prisma/client"; import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; import { emailSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { StatusError } from "@stackframe/stack-shared/dist/utils/errors"; export const contactChannelVerificationCodeHandler = createVerificationCodeHandler({ metadata: { @@ -44,15 +45,26 @@ export const contactChannelVerificationCodeHandler = createVerificationCodeHandl }); }, async handler(tenancy, { email }, data) { - await prismaClient.contactChannel.update({ - where: { - tenancyId_projectUserId_type_value: { - tenancyId: tenancy.id, - projectUserId: data.user_id, - type: "EMAIL", - value: email, - }, + const uniqueKeys = { + tenancyId_projectUserId_type_value: { + tenancyId: tenancy.id, + projectUserId: data.user_id, + type: "EMAIL", + value: email, }, + } as const; + + const contactChannel = await prismaClient.contactChannel.findUnique({ + where: uniqueKeys, + }); + + // This happens if the email is sent but then before the user clicks the link, the contact channel is deleted. + if (!contactChannel) { + throw new StatusError(400, "Contact channel not found. Was your contact channel deleted?"); + } + + await prismaClient.contactChannel.update({ + where: uniqueKeys, data: { isVerified: true, } diff --git a/apps/backend/src/app/api/latest/users/crud.tsx b/apps/backend/src/app/api/latest/users/crud.tsx index 2af84e8ff..c9df4a88e 100644 --- a/apps/backend/src/app/api/latest/users/crud.tsx +++ b/apps/backend/src/app/api/latest/users/crud.tsx @@ -687,18 +687,34 @@ export const usersCrudHandlers = createLazyProxy(() => createCrudHandlers(usersC }); if (data.selected_team_id !== null) { - await tx.teamMember.update({ - where: { - tenancyId_projectUserId_teamId: { + try { + await tx.teamMember.update({ + where: { + tenancyId_projectUserId_teamId: { + tenancyId: auth.tenancy.id, + projectUserId: params.user_id, + teamId: data.selected_team_id, + }, + }, + data: { + isSelected: BooleanTrue.TRUE, + }, + }); + } catch (e) { + const members = await prismaClient.teamMember.findMany({ + where: { tenancyId: auth.tenancy.id, projectUserId: params.user_id, - teamId: data.selected_team_id, - }, - }, - data: { - isSelected: BooleanTrue.TRUE, - }, - }); + } + }); + throw new StackAssertionError("Failed to update team member", { + error: e, + tenancy_id: auth.tenancy.id, + user_id: params.user_id, + team_id: data.selected_team_id, + members, + }); + } } } diff --git a/apps/backend/src/lib/emails.tsx b/apps/backend/src/lib/emails.tsx index 85e14c0f2..4df4c3457 100644 --- a/apps/backend/src/lib/emails.tsx +++ b/apps/backend/src/lib/emails.tsx @@ -94,9 +94,9 @@ async function _sendEmailWithoutRetries(options: SendEmailOptions): Promise { toArray = (await Promise.all(toArray.map(async (to) => { const emailableResponse = await fetch(`https://api.emailable.com/v1/verify?email=${encodeURIComponent(options.to as string)}&api_key=${emailableApiKey}`); 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 e69942529..7cd55b2e8 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 @@ -94,7 +94,7 @@ function EditDialog(props: { open={props.open} defaultValues={{ addWww: props.type === 'create', - domain: props.type === 'update' ? props.defaultDomain.replace(/^https:\/\//, "") : undefined, + domain: props.type === 'update' ? props.defaultDomain.replace(/^https?:\/\//, "") : undefined, handlerPath: props.type === 'update' ? props.defaultHandlerPath : "/handler", insecureHttp: false, }} @@ -128,7 +128,7 @@ function EditDialog(props: { domains: [...props.domains].map((domain, i) => { if (i === props.editIndex) { return { - domain: values.domain, + domain: (values.insecureHttp ? 'http://' : 'https://') + values.domain, handlerPath: values.handlerPath, }; } diff --git a/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-up.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-up.test.ts index e1a4c6f34..9cd9967fc 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-up.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/auth/password/sign-up.test.ts @@ -56,6 +56,35 @@ it("should sign up new users", async ({ expect }) => { `); }); +it("should not sign up new users if verification callback url is not valid", async ({ expect }) => { + const mailbox = backendContext.value.mailbox; + const email = mailbox.emailAddress; + const password = generateSecureRandomString(); + const response = await niceBackendFetch("/api/v1/auth/password/sign-up", { + method: "POST", + accessType: "client", + body: { + email, + password, + verification_callback_url: "http://invalid-domain.com", + }, + }); + + expect(response).toMatchInlineSnapshot(` + NiceResponse { + "status": 400, + "body": { + "code": "REDIRECT_URL_NOT_WHITELISTED", + "error": "Redirect URL not whitelisted. Did you forget to add this domain to the trusted domains list on the Stack Auth dashboard?", + }, + "headers": Headers { + "x-stack-known-error": "REDIRECT_URL_NOT_WHITELISTED", +