From 778206cb5e0a7cbf006b3e229253dc1335ab6181 Mon Sep 17 00:00:00 2001 From: Madison Date: Thu, 26 Jun 2025 10:50:34 -0500 Subject: [PATCH 1/7] Updates STACK_EMAILABLE_API_KEY to have default value (#727) Fixes #725 --- apps/backend/src/lib/emails.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/backend/src/lib/emails.tsx b/apps/backend/src/lib/emails.tsx index 85e14c0f2..95614f6bc 100644 --- a/apps/backend/src/lib/emails.tsx +++ b/apps/backend/src/lib/emails.tsx @@ -95,7 +95,7 @@ async function _sendEmailWithoutRetries(options: SendEmailOptions): Promise { toArray = (await Promise.all(toArray.map(async (to) => { From cc8de671326fe16ee7dac1699ef117633c31d21b Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Thu, 26 Jun 2025 11:51:04 -0400 Subject: [PATCH 2/7] Only use Emailable for shared configs --- apps/backend/src/lib/emails.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/lib/emails.tsx b/apps/backend/src/lib/emails.tsx index 95614f6bc..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}`); From 4d8856ed17dfe432e9b372d077cc31836d484d6a Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Thu, 26 Jun 2025 11:57:52 -0400 Subject: [PATCH 3/7] Improve note on Docker envvars --- docker/server/.env | 1 + docker/server/.env.example | 2 ++ docs/templates/others/self-host.mdx | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/server/.env b/docker/server/.env index 951910213..4d3d79bd3 100644 --- a/docker/server/.env +++ b/docker/server/.env @@ -22,6 +22,7 @@ STACK_EMAIL_PORT= STACK_EMAIL_USERNAME= STACK_EMAIL_PASSWORD= STACK_EMAIL_SENDER= +STACK_EMAILABLE_API_KEY= # Set these if you want to use webhooks STACK_SVIX_SERVER_URL=# this is only needed if you self-host the Svix service diff --git a/docker/server/.env.example b/docker/server/.env.example index a8f1f682b..7ca20d971 100644 --- a/docker/server/.env.example +++ b/docker/server/.env.example @@ -1,3 +1,5 @@ +# IMPORTANT: YOU MUST REGENERATE THE STACK_SERVER_SECRET VALUE BELOW + NEXT_PUBLIC_STACK_API_URL=http://localhost:8102 NEXT_PUBLIC_STACK_DASHBOARD_URL=http://localhost:8101 diff --git a/docs/templates/others/self-host.mdx b/docs/templates/others/self-host.mdx index 0f1f65965..e242838a2 100644 --- a/docs/templates/others/self-host.mdx +++ b/docs/templates/others/self-host.mdx @@ -34,7 +34,7 @@ Stack Auth provides a [pre-configured Docker](https://hub.docker.com/r/stackauth docker run -d --name db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=password -e POSTGRES_DB=stackframe -p 5432:5432 postgres:latest ``` -2. Get the [example environment file](https://github.com/stack-auth/stack-auth/tree/main/docker/server/.env.example) and modify it to your needs. See the [full template here](https://github.com/stack-auth/stack-auth/blob/dev/docker/server/.env). +2. Get the [example environment file](https://github.com/stack-auth/stack-auth/tree/main/docker/server/.env.example) and modify it to your needs (for security, you MUST edit at least the `STACK_SERVER_SECRET` value). See the [full template here](https://github.com/stack-auth/stack-auth/blob/dev/docker/server/.env). 3. Run the Docker container: ```sh From a38551eda18b7da6f77322b1a2af7e07688ad59b Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Thu, 26 Jun 2025 20:49:07 +0200 Subject: [PATCH 4/7] Fix domain page bug (#709) ---- > [!IMPORTANT] > Fixes domain protocol handling in `EditDialog` in `page-client.tsx` to correctly strip and apply `http://` or `https://` based on user input. > > - **Behavior**: > - Fixes domain protocol handling in `EditDialog` in `page-client.tsx`. > - Updates `defaultDomain` to strip both `http://` and `https://` prefixes. > - Ensures domain is saved with correct protocol (`http://` or `https://`) based on `insecureHttp` value. > - **Misc**: > - Minor changes to domain update logic in `EditDialog` to ensure correct protocol is applied. > > This description was created by [Ellipsis](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral) for 9ab6d979ba171cfbe9ef679ca3209357a7b3d814. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed. --------- Co-authored-by: Konsti Wohlwend --- .../(protected)/projects/[projectId]/domains/page-client.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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, }; } From 8139ee926b3a7cc384c955da0b2707fdd831c856 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Fri, 27 Jun 2025 02:20:08 +0200 Subject: [PATCH 5/7] Added error logs for team member update (#706) ---- > [!IMPORTANT] > Added error handling for team member updates and contact channel verification, with corresponding tests. > > - **Error Handling**: > - In `verification-code-handler.tsx`, added `StatusError` for missing contact channels during email verification. > - In `crud.tsx`, wrapped `teamMember.update` in a try-catch to log errors with `StackAssertionError` if update fails. > - **Testing**: > - Updated `users.test.ts` to test team selection updates, ensuring correct team ID is set or unset. > > This description was created by [Ellipsis](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral) for b024f7ba3e2edc9546c02b6d1138b659d0b904c7. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed. --------- Co-authored-by: Konsti Wohlwend Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- .../verify/verification-code-handler.tsx | 28 ++++-- .../backend/src/app/api/latest/users/crud.tsx | 36 +++++-- .../backend/endpoints/api/v1/users.test.ts | 98 +++---------------- 3 files changed, 60 insertions(+), 102 deletions(-) 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..ebea0c284 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(404, "Contact channel not found. Did you maybe delete your contact channel?"); + } + + 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/e2e/tests/backend/endpoints/api/v1/users.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts index 850e07de7..fa999010b 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/users.test.ts @@ -700,106 +700,36 @@ describe("with client access", () => { it("should be able to update selected team", async ({ expect }) => { await Auth.Otp.signIn(); - const { teamId } = await Team.createWithCurrentAsCreator({}); + const { teamId: team1Id } = await Team.createWithCurrentAsCreator({}); + const { teamId: team2Id } = await Team.createWithCurrentAsCreator({}); const response1 = await niceBackendFetch("/api/v1/users/me", { accessType: "client", }); - expect(response1).toMatchInlineSnapshot(` - NiceResponse { - "status": 200, - "body": { - "auth_with_email": true, - "client_metadata": null, - "client_read_only_metadata": null, - "display_name": null, - "has_password": false, - "id": "", - "is_anonymous": false, - "oauth_providers": [], - "otp_auth_enabled": true, - "passkey_auth_enabled": false, - "primary_email": "default-mailbox--@stack-generated.example.com", - "primary_email_verified": true, - "profile_image_url": null, - "requires_totp_mfa": false, - "selected_team": null, - "selected_team_id": null, - "signed_up_at_millis": , - }, - "headers": Headers {