Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/backend/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ async function seed() {
allow_localhost: allowLocalhost,
domains: [
...(dashboardDomain && new URL(dashboardDomain).hostname !== 'localhost' ? [{ domain: dashboardDomain, handler_path: '/handler' }] : []),
...internalTenancy.config.domains.filter((d) => d.domain !== dashboardDomain),
...Object.values(internalTenancy.config.domains.trustedDomains)
.filter((d) => d.baseUrl !== dashboardDomain && d.baseUrl)
Comment thread
fomalhautb marked this conversation as resolved.
.map((d) => ({ domain: d.baseUrl || throwErr('Domain base URL is required'), handler_path: d.handlerPath })),
]
},
},
Expand Down
20 changes: 9 additions & 11 deletions apps/backend/src/app/api/latest/(api-keys)/handlers.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { listPermissions } from "@/lib/permissions";
import { Tenancy } from "@/lib/tenancies";
import { getPrismaClientForTenancy } from "@/prisma-client";
import { createCrudHandlers } from "@/route-handlers/crud-handler";
import { SmartRequestAuth } from "@/route-handlers/smart-request";
Expand All @@ -15,16 +16,13 @@ import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids";
import * as yup from "yup";


async function throwIfFeatureDisabled(project: {
allow_team_api_keys: boolean,
allow_user_api_keys: boolean,
}, type: "team" | "user") {
async function throwIfFeatureDisabled(tenancy: Tenancy, type: "team" | "user") {
if (type === "team") {
if (!project.allow_team_api_keys) {
if (!tenancy.config.apiKeys.enabled.team) {
throw new StatusError(StatusError.BadRequest, "Team API keys are not enabled for this project.");
}
} else {
if (!project.allow_user_api_keys) {
if (!tenancy.config.apiKeys.enabled.user) {
throw new StatusError(StatusError.BadRequest, "User API keys are not enabled for this project.");
}
}
Expand Down Expand Up @@ -175,7 +173,7 @@ function createApiKeyHandlers<Type extends "user" | "team">(type: Type) {
body: type === 'user' ? userApiKeysCreateOutputSchema.defined() : teamApiKeysCreateOutputSchema.defined(),
}),
handler: async ({ url, auth, body }) => {
await throwIfFeatureDisabled(auth.tenancy.config, type);
await throwIfFeatureDisabled(auth.tenancy, type);
const { userId, teamId } = await parseTypeAndParams({ type, params: body });
await ensureUserCanManageApiKeys(auth, {
userId,
Expand Down Expand Up @@ -247,7 +245,7 @@ function createApiKeyHandlers<Type extends "user" | "team">(type: Type) {
body: (type === 'user' ? userApiKeysCrud : teamApiKeysCrud).server.readSchema.defined(),
}),
handler: async ({ auth, body }) => {
await throwIfFeatureDisabled(auth.tenancy.config, type);
await throwIfFeatureDisabled(auth.tenancy, type);
const prisma = await getPrismaClientForTenancy(auth.tenancy);

const apiKey = await prisma.projectApiKey.findUnique({
Expand Down Expand Up @@ -297,7 +295,7 @@ function createApiKeyHandlers<Type extends "user" | "team">(type: Type) {
}),

onList: async ({ auth, query }) => {
await throwIfFeatureDisabled(auth.tenancy.config, type);
await throwIfFeatureDisabled(auth.tenancy, type);
const { userId, teamId } = await parseTypeAndParams({ type, params: query });
await ensureUserCanManageApiKeys(auth, {
userId,
Expand All @@ -323,7 +321,7 @@ function createApiKeyHandlers<Type extends "user" | "team">(type: Type) {
},

onRead: async ({ auth, query, params }) => {
await throwIfFeatureDisabled(auth.tenancy.config, type);
await throwIfFeatureDisabled(auth.tenancy, type);

const prisma = await getPrismaClientForTenancy(auth.tenancy);

Expand All @@ -348,7 +346,7 @@ function createApiKeyHandlers<Type extends "user" | "team">(type: Type) {
},

onUpdate: async ({ auth, data, params, query }) => {
await throwIfFeatureDisabled(auth.tenancy.config, type);
await throwIfFeatureDisabled(auth.tenancy, type);

const prisma = await getPrismaClientForTenancy(auth.tenancy);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,13 @@ export const GET = createSmartRouteHandler({
throw new KnownErrors.InvalidPublishableClientKey(tenancy.project.id);
}

const provider = tenancy.config.oauth_providers.find((p) => p.id === params.provider_id);
if (!provider) {
const providerRaw = Object.entries(tenancy.config.auth.oauth.providers).find(([providerId, _]) => providerId === params.provider_id);
if (!providerRaw) {
throw new KnownErrors.OAuthProviderNotFoundOrNotEnabled();
}

const provider = { id: providerRaw[0], ...providerRaw[1] };

// If the authorization token is present, we are adding new scopes to the user instead of sign-in/sign-up
let projectUserId: string | undefined;
if (query.type === "link") {
Expand All @@ -85,7 +87,7 @@ export const GET = createSmartRouteHandler({
throw new StatusError(StatusError.Forbidden, "The access token is not valid for this branch");
}

if (query.provider_scope && provider.type === "shared") {
if (query.provider_scope && provider.isShared) {
throw new KnownErrors.OAuthExtraScopeNotAvailableWithSharedOAuthKeys();
}
projectUserId = userId;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async function createProjectUserOAuthAccount(prisma: PrismaClient, params: {
}

const redirectOrThrowError = (error: KnownError, tenancy: Tenancy, errorRedirectUrl?: string) => {
if (!errorRedirectUrl || !validateRedirectUrl(errorRedirectUrl, tenancy.config.domains, tenancy.config.allow_localhost)) {
if (!errorRedirectUrl || !validateRedirectUrl(errorRedirectUrl, tenancy)) {
throw error;
}

Expand Down Expand Up @@ -119,12 +119,14 @@ const handler = createSmartRouteHandler({
throw new KnownErrors.OuterOAuthTimeout();
}

const provider = tenancy.config.oauth_providers.find((p) => p.id === params.provider_id);
if (!provider) {
const providerRaw = Object.entries(tenancy.config.auth.oauth.providers).find(([providerId, _]) => providerId === params.provider_id);
if (!providerRaw) {
throw new KnownErrors.OAuthProviderNotFoundOrNotEnabled();
}

const providerObj = await getProvider(provider);
const provider = { id: providerRaw[0], ...providerRaw[1] };

const providerObj = await getProvider(provider as any);
let callbackResult: Awaited<ReturnType<typeof providerObj.getCallback>>;
try {
callbackResult = await providerObj.getCallback({
Expand Down Expand Up @@ -279,7 +281,7 @@ const handler = createSmartRouteHandler({

// ========================== sign up user ==========================

if (!tenancy.config.sign_up_enabled) {
if (!tenancy.config.auth.allowSignUp) {
throw new KnownErrors.SignUpNotEnabled();
}

Expand All @@ -298,7 +300,7 @@ const handler = createSmartRouteHandler({

// Check if we should link this OAuth account to an existing user based on email
if (oldContactChannel && oldContactChannel.usedForAuth) {
const oauthAccountMergeStrategy = tenancy.config.oauth_account_merge_strategy;
const oauthAccountMergeStrategy = tenancy.config.auth.oauth.accountMergeStrategy;
switch (oauthAccountMergeStrategy) {
case 'link_method': {
if (!oldContactChannel.isVerified) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ export const POST = createSmartRouteHandler({
}).defined(),
}),
async handler({ auth: { tenancy }, body: { email, callback_url: callbackUrl }, clientVersion }, fullReq) {
if (!tenancy.config.magic_link_enabled) {
throw new StatusError(StatusError.Forbidden, "Magic link is not enabled for this project");
if (!tenancy.config.auth.otp.allowSignIn) {
throw new StatusError(StatusError.Forbidden, "OTP sign-in is not enabled for this project");
}

const user = await ensureUserForEmailAllowsOtp(tenancy, email);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export async function ensureUserForEmailAllowsOtp(tenancy: Tenancy, email: strin
throw new KnownErrors.UserWithEmailAlreadyExists(contactChannel.value, true);
}
} else {
if (!tenancy.config.sign_up_enabled) {
if (!tenancy.config.auth.allowSignUp) {
throw new KnownErrors.SignUpNotEnabled();
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const POST = createSmartRouteHandler({
}),
async handler({ auth: { tenancy } }) {

if (!tenancy.config.passkey_enabled) {
if (!tenancy.config.auth.passkey.allowSignIn) {
throw new KnownErrors.PasskeyAuthenticationNotEnabled();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const POST = createSmartRouteHandler({
}),
}),
async handler({ auth: { tenancy, user } }) {
if (!tenancy.config.passkey_enabled) {
if (!tenancy.config.auth.passkey.allowSignIn) {
throw new KnownErrors.PasskeyAuthenticationNotEnabled();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const registerVerificationCodeHandler = createVerificationCodeHandler({
throw new StackAssertionError("send() called on a Passkey registration verification code handler");
},
async handler(tenancy, _, { challenge }, { credential }, user) {
if (!tenancy.config.passkey_enabled) {
if (!tenancy.config.auth.passkey.allowSignIn) {
throw new KnownErrors.PasskeyAuthenticationNotEnabled();
}

Expand All @@ -55,7 +55,7 @@ export const registerVerificationCodeHandler = createVerificationCodeHandler({
let expectedOrigin = "";
const clientDataJSON = decodeClientDataJSON(credential.response.clientDataJSON);
const { origin } = clientDataJSON;
const localhostAllowed = tenancy.config.allow_localhost;
const localhostAllowed = tenancy.config.domains.allowLocalhost;
const parsedOrigin = new URL(origin);
const isLocalhost = parsedOrigin.hostname === "localhost";

Expand All @@ -69,7 +69,10 @@ export const registerVerificationCodeHandler = createVerificationCodeHandler({
}

if (!isLocalhost) {
if (!tenancy.config.domains.map(e => e.domain).includes(parsedOrigin.origin)) {
if (!Object.values(tenancy.config.domains.trustedDomains)
.filter(e => e.baseUrl)
.map(e => e.baseUrl)
.includes(parsedOrigin.origin)) {
throw new KnownErrors.PasskeyAuthenticationFailed("Passkey registration failed because the origin is not allowed");
} else {
expectedRPID = parsedOrigin.hostname;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const passkeySignInVerificationCodeHandler = createVerificationCodeHandle
},
async handler(tenancy, _, { challenge }, { authentication_response }) {

if (!tenancy.config.passkey_enabled) {
if (!tenancy.config.auth.passkey.allowSignIn) {
throw new KnownErrors.PasskeyAuthenticationNotEnabled();
}

Expand Down Expand Up @@ -67,7 +67,7 @@ export const passkeySignInVerificationCodeHandler = createVerificationCodeHandle
let expectedOrigin = "";
const clientDataJSON = decodeClientDataJSON(authentication_response.response.clientDataJSON);
const { origin } = clientDataJSON;
const localhostAllowed = tenancy.config.allow_localhost;
const localhostAllowed = tenancy.config.domains.allowLocalhost;
const parsedOrigin = new URL(origin);
const isLocalhost = parsedOrigin.hostname === "localhost";

Expand All @@ -81,7 +81,10 @@ export const passkeySignInVerificationCodeHandler = createVerificationCodeHandle
}

if (!isLocalhost) {
if (!tenancy.config.domains.map(e => e.domain).includes(parsedOrigin.origin)) {
if (!Object.values(tenancy.config.domains.trustedDomains)
.filter(e => e.baseUrl)
.map(e => e.baseUrl)
.includes(parsedOrigin.origin)) {
throw new KnownErrors.PasskeyAuthenticationFailed("Passkey authentication failed because the origin is not allowed");
} else {
expectedRPID = parsedOrigin.hostname;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const resetPasswordVerificationCodeHandler = createVerificationCodeHandle
});
},
async handler(tenancy, { email }, data, { password }) {
if (!tenancy.config.credential_enabled) {
if (!tenancy.config.auth.password.allowSignIn) {
throw new KnownErrors.PasswordAuthenticationNotEnabled();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const POST = createSmartRouteHandler({
}).defined(),
}),
async handler({ auth: { tenancy }, body: { email, callback_url: callbackUrl } }, fullReq) {
if (!tenancy.config.credential_enabled) {
if (!tenancy.config.auth.password.allowSignIn) {
throw new KnownErrors.PasswordAuthenticationNotEnabled();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const POST = createSmartRouteHandler({
bodyType: yupString().oneOf(["success"]).defined(),
}),
async handler({ auth: { tenancy, user }, body: { password } }) {
if (!tenancy.config.credential_enabled) {
if (!tenancy.config.auth.password.allowSignIn) {
throw new KnownErrors.PasswordAuthenticationNotEnabled();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const POST = createSmartRouteHandler({
}).defined(),
}),
async handler({ auth: { tenancy }, body: { email, password } }, fullReq) {
if (!tenancy.config.credential_enabled) {
if (!tenancy.config.auth.password.allowSignIn) {
throw new KnownErrors.PasswordAuthenticationNotEnabled();
}

Expand Down
10 changes: 3 additions & 7 deletions apps/backend/src/app/api/latest/auth/password/sign-up/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,11 @@ export const POST = createSmartRouteHandler({
}).defined(),
}),
async handler({ auth: { tenancy }, body: { email, password, verification_callback_url: verificationCallbackUrl } }, fullReq) {
if (!tenancy.config.credential_enabled) {
if (!tenancy.config.auth.password.allowSignIn) {
throw new KnownErrors.PasswordAuthenticationNotEnabled();
}

if (!validateRedirectUrl(
verificationCallbackUrl,
tenancy.config.domains,
tenancy.config.allow_localhost,
)) {
if (!validateRedirectUrl(verificationCallbackUrl, tenancy)) {
throw new KnownErrors.RedirectUrlNotWhitelisted();
}

Expand All @@ -53,7 +49,7 @@ export const POST = createSmartRouteHandler({
throw passwordError;
}

if (!tenancy.config.sign_up_enabled) {
if (!tenancy.config.auth.allowSignUp) {
throw new KnownErrors.SignUpNotEnabled();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const POST = createSmartRouteHandler({
bodyType: yupString().oneOf(["success"]).defined(),
}),
async handler({ auth: { tenancy, user }, body: { old_password, new_password }, headers: { "x-stack-refresh-token": refreshToken } }, fullReq) {
if (!tenancy.config.credential_enabled) {
if (!tenancy.config.auth.password.allowSignIn) {
throw new KnownErrors.PasswordAuthenticationNotEnabled();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ export const connectedAccountAccessTokenCrudHandlers = createLazyProxy(() => cre
throw new StatusError(StatusError.Forbidden, "Client can only access its own connected accounts");
}

const provider = auth.tenancy.config.oauth_providers.find((p) => p.id === params.provider_id);
if (!provider) {
const providerRaw = Object.entries(auth.tenancy.config.auth.oauth.providers).find(([providerId, _]) => providerId === params.provider_id);
if (!providerRaw) {
throw new KnownErrors.OAuthProviderNotFoundOrNotEnabled();
}

if (provider.type === 'shared' && getEnvVariable('STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS') !== 'true') {
const provider = { id: providerRaw[0], ...providerRaw[1] };

if (provider.isShared && getEnvVariable('STACK_ALLOW_SHARED_OAUTH_ACCESS_TOKENS') !== 'true') {
throw new KnownErrors.OAuthAccessTokenNotAvailableWithSharedOAuthKeys();
}

Expand Down
11 changes: 7 additions & 4 deletions apps/backend/src/app/api/latest/emails/render-email/route.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { getEmailThemeForTemplate, renderEmailWithTemplate } from "@/lib/email-rendering";
import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler";
import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors";
import { adaptSchema, templateThemeIdSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors";
import { adaptSchema, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields";
import { StatusError } from "@stackframe/stack-shared/dist/utils/errors";

import { templateThemeIdSchema } from "@stackframe/stack-shared/dist/schema-fields";

export const POST = createSmartRouteHandler({
metadata: {
Expand Down Expand Up @@ -38,10 +40,11 @@ export const POST = createSmartRouteHandler({
if ((!body.template_id && !body.template_tsx_source) || (body.template_id && body.template_tsx_source)) {
throw new StatusError(400, "Exactly one of template_id or template_tsx_source must be provided");
}
if (body.theme_id && !(body.theme_id in tenancy.completeConfig.emails.themes)) {

if (body.theme_id && !(body.theme_id in tenancy.config.emails.themes)) {
throw new StatusError(400, "No theme found with given id");
}
const templateList = new Map(Object.entries(tenancy.completeConfig.emails.templates));
const templateList = new Map(Object.entries(tenancy.config.emails.templates));
const themeSource = body.theme_id === undefined ? body.theme_tsx_source! : getEmailThemeForTemplate(tenancy, body.theme_id);
const templateSource = body.template_id ? templateList.get(body.template_id)?.tsxSource : body.template_tsx_source;

Expand Down
8 changes: 4 additions & 4 deletions apps/backend/src/app/api/latest/emails/send-email/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,19 @@ export const POST = createSmartRouteHandler({
if (!getEnvVariable("STACK_FREESTYLE_API_KEY")) {
throw new StatusError(500, "STACK_FREESTYLE_API_KEY is not set");
}
if (auth.tenancy.config.email_config.type === "shared") {
if (auth.tenancy.config.emails.server.isShared) {
throw new StatusError(400, "Cannot send custom emails when using shared email config");
}
const emailConfig = await getEmailConfig(auth.tenancy);
const notificationCategory = getNotificationCategoryByName(body.notification_category_name);
if (!notificationCategory) {
throw new StatusError(404, "Notification category not found");
}
const themeList = auth.tenancy.completeConfig.emails.themes;
if (!Object.keys(themeList).includes(auth.tenancy.completeConfig.emails.selectedThemeId)) {
const themeList = auth.tenancy.config.emails.themes;
if (!Object.keys(themeList).includes(auth.tenancy.config.emails.selectedThemeId)) {
throw new StatusError(400, "No active theme found");
}
const activeTheme = themeList[auth.tenancy.completeConfig.emails.selectedThemeId];
const activeTheme = themeList[auth.tenancy.config.emails.selectedThemeId];

const prisma = await getPrismaClientForTenancy(auth.tenancy);

Expand Down
Loading