Merge dev into added_docs

This commit is contained in:
Konsti Wohlwend 2025-06-27 04:32:37 -07:00 committed by GitHub
commit 3bc096a12a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 2894 additions and 2713 deletions

View File

@ -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;

View File

@ -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,
}

View File

@ -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,
});
}
}
}

View File

@ -94,9 +94,9 @@ async function _sendEmailWithoutRetries(options: SendEmailOptions): Promise<Resu
try {
let toArray = typeof options.to === 'string' ? [options.to] : options.to;
// use Emailable to check if the email is valid. skip the ones that are not (it's as if they had bounced)
const emailableApiKey = getEnvVariable('STACK_EMAILABLE_API_KEY');
if (emailableApiKey) {
// If using the shared email config, use Emailable to check if the email is valid. skip the ones that are not (it's as if they had bounced)
const emailableApiKey = getEnvVariable('STACK_EMAILABLE_API_KEY', "");
if (options.emailConfig.type === 'shared' && emailableApiKey) {
await traceSpan('verifying email addresses with Emailable', async () => {
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}`);

View File

@ -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,
};
}

View File

@ -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",
<some fields may have been hidden>,
},
}
`);
});
it("should not allow signing up with an e-mail that already exists", async ({ expect }) => {
await Auth.Password.signUpWithEmail();
const res2 = await niceBackendFetch("/api/v1/auth/password/sign-up", {

View File

@ -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": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@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": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
expect(response1.body.selected_team_id).toEqual(null);
const response2 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
selected_team_id: teamId,
selected_team_id: team1Id,
},
});
expect(response2).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@stack-generated.example.com",
"primary_email_verified": true,
"profile_image_url": null,
"requires_totp_mfa": false,
"selected_team": {
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": "New Team",
"id": "<stripped UUID>",
"profile_image_url": null,
},
"selected_team_id": "<stripped UUID>",
"signed_up_at_millis": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
expect(response2.body.selected_team_id).toEqual(teamId);
expect(response2.body.selected_team_id).toEqual(team1Id);
const response3 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
selected_team_id: team2Id,
},
});
expect(response3.body.selected_team_id).toEqual(team2Id);
const response4 = await niceBackendFetch("/api/v1/users/me", {
accessType: "client",
method: "PATCH",
body: {
selected_team_id: null,
},
});
expect(response3).toMatchInlineSnapshot(`
NiceResponse {
"status": 200,
"body": {
"auth_with_email": true,
"client_metadata": null,
"client_read_only_metadata": null,
"display_name": null,
"has_password": false,
"id": "<stripped UUID>",
"is_anonymous": false,
"oauth_providers": [],
"otp_auth_enabled": true,
"passkey_auth_enabled": false,
"primary_email": "default-mailbox--<stripped UUID>@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": <stripped field 'signed_up_at_millis'>,
},
"headers": Headers { <some fields may have been hidden> },
}
`);
expect(response4.body.selected_team_id).toEqual(null);
});
});

View File

@ -17,7 +17,7 @@ export async function scaffoldProject(body?: Omit<AdminProjectCreateOptions, 'di
Result.orThrow(await internalApp.signUpWithCredential({
email: fakeEmail,
password: "password",
verificationCallbackUrl: "https://stack-js-test.example.com/verify",
verificationCallbackUrl: "http://localhost:3000",
}));
const adminUser = await internalApp.getUser({
or: 'throw',

View File

@ -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

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff