mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Merge dev into added_docs
This commit is contained in:
commit
3bc096a12a
@ -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;
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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", {
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
2
docs/templates/others/self-host.mdx
vendored
2
docs/templates/others/self-host.mdx
vendored
@ -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
Loading…
Reference in New Issue
Block a user