diff --git a/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx b/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx index 3f8fd3ff4..f68241b3b 100644 --- a/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx +++ b/apps/backend/src/app/api/latest/auth/oauth/callback/[provider_id]/route.tsx @@ -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, Object.values(tenancy.config.domains), tenancy.config.domains.allowLocalhost)) { throw error; } @@ -119,12 +119,17 @@ 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>; try { callbackResult = await providerObj.getCallback({ @@ -279,7 +284,7 @@ const handler = createSmartRouteHandler({ // ========================== sign up user ========================== - if (!tenancy.config.sign_up_enabled) { + if (!tenancy.config.auth.allowSignUp) { throw new KnownErrors.SignUpNotEnabled(); } @@ -298,7 +303,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) { diff --git a/apps/backend/src/app/api/latest/emails/render-email/route.tsx b/apps/backend/src/app/api/latest/emails/render-email/route.tsx index 9832e4313..f812806bd 100644 --- a/apps/backend/src/app/api/latest/emails/render-email/route.tsx +++ b/apps/backend/src/app/api/latest/emails/render-email/route.tsx @@ -2,7 +2,7 @@ import { renderEmailWithTemplate } from "@/lib/email-rendering"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors"; import { adaptSchema, yupMixed, yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { captureError, StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError, StatusError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; export const POST = createSmartRouteHandler({ @@ -40,8 +40,8 @@ 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"); } - const themeList = new Map(Object.entries(tenancy.completeConfig.emails.themeList)); - const templateList = new Map(Object.entries(tenancy.completeConfig.emails.templateList)); + const themeList = new Map(Object.entries(tenancy.config.emails.themeList)); + const templateList = new Map(Object.entries(tenancy.config.emails.templateList)); const themeSource = body.theme_id ? themeList.get(body.theme_id)?.tsxSource : body.theme_tsx_source; const templateSource = body.template_id ? templateList.get(body.template_id)?.tsxSource : body.template_tsx_source; if (!themeSource) { diff --git a/apps/backend/src/app/api/latest/internal/environment-config/current/crud.tsx b/apps/backend/src/app/api/latest/internal/environment-config/current/crud.tsx new file mode 100644 index 000000000..d251a3606 --- /dev/null +++ b/apps/backend/src/app/api/latest/internal/environment-config/current/crud.tsx @@ -0,0 +1,39 @@ +import { overrideEnvironmentConfigOverride } from "@/lib/config"; +import { getPrismaClientForTenancy } from "@/prisma-client"; +import { createCrudHandlers } from "@/route-handlers/crud-handler"; +import { environmentConfigCrud } from "@stackframe/stack-shared/dist/interface/crud/environment-config"; +import { yupObject } from "@stackframe/stack-shared/dist/schema-fields"; +import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; + +export const environmentConfigCrudHandlers = createLazyProxy(() => createCrudHandlers(environmentConfigCrud, { + paramsSchema: yupObject({}), + onRead: async ({ auth }) => { + return { + id: auth.tenancy.id, + project_id: auth.project.id, + branch_id: auth.tenancy.branchId, + organization_id: auth.tenancy.organization?.id, + config: auth.tenancy.completeConfig, + }; + }, + onUpdate: async ({ auth, data }) => { + const prisma = await getPrismaClientForTenancy(auth.tenancy); + + if (data.config) { + await overrideEnvironmentConfigOverride({ + tx: prisma, + projectId: auth.project.id, + branchId: auth.tenancy.branchId, + environmentConfigOverrideOverride: data.config, + }); + } + + return { + id: auth.tenancy.id, + project_id: auth.project.id, + branch_id: auth.tenancy.branchId, + organization_id: auth.tenancy.organization?.id, + config: auth.tenancy.completeConfig, + }; + }, +})); diff --git a/apps/backend/src/app/api/latest/projects/current/crud.tsx b/apps/backend/src/app/api/latest/projects/current/crud.tsx index 6f230c156..7d4b2c8c7 100644 --- a/apps/backend/src/app/api/latest/projects/current/crud.tsx +++ b/apps/backend/src/app/api/latest/projects/current/crud.tsx @@ -1,3 +1,4 @@ +import { renderedOrganizationConfigToProjectCrud } from "@/lib/config"; import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { clientProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { yupObject } from "@stackframe/stack-shared/dist/schema-fields"; @@ -8,7 +9,7 @@ export const clientProjectsCrudHandlers = createLazyProxy(() => createCrudHandle onRead: async ({ auth }) => { return { ...auth.project, - config: auth.tenancy.config, + config: renderedOrganizationConfigToProjectCrud(auth.tenancy.config), }; }, })); diff --git a/apps/backend/src/lib/redirect-urls.tsx b/apps/backend/src/lib/redirect-urls.tsx index 08e0b2d36..aa70255d1 100644 --- a/apps/backend/src/lib/redirect-urls.tsx +++ b/apps/backend/src/lib/redirect-urls.tsx @@ -1,7 +1,7 @@ import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; import { createUrlIfValid, isLocalhost } from "@stackframe/stack-shared/dist/utils/urls"; -export function validateRedirectUrl(urlOrString: string | URL, domains: { domain: string, handler_path: string }[], allowLocalhost: boolean): boolean { +export function validateRedirectUrl(urlOrString: string | URL, domains: { baseUrl: string }[], allowLocalhost: boolean): boolean { const url = createUrlIfValid(urlOrString); if (!url) return false; if (allowLocalhost && isLocalhost(url)) { @@ -9,10 +9,10 @@ export function validateRedirectUrl(urlOrString: string | URL, domains: { domain } return domains.some((domain) => { const testUrl = url; - const baseUrl = createUrlIfValid(domain.domain); + const baseUrl = createUrlIfValid(domain.baseUrl); if (!baseUrl) { captureError("invalid-redirect-domain", new StackAssertionError("Invalid redirect domain; maybe this should be fixed in the database", { - domain: domain.domain, + domain: domain.baseUrl, })); return false; } diff --git a/apps/backend/src/lib/tenancies.tsx b/apps/backend/src/lib/tenancies.tsx index 25eeec6ed..bb99f0f9d 100644 --- a/apps/backend/src/lib/tenancies.tsx +++ b/apps/backend/src/lib/tenancies.tsx @@ -2,7 +2,7 @@ import { globalPrismaClient, rawQuery } from "@/prisma-client"; import { Prisma } from "@prisma/client"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; -import { getRenderedOrganizationConfigQuery, renderedOrganizationConfigToProjectCrud } from "./config"; +import { getRenderedOrganizationConfigQuery } from "./config"; import { getProject } from "./projects"; /** @@ -31,13 +31,10 @@ export async function tenancyPrismaToCrud(prisma: Prisma.TenancyGetPayload<{}>) branchId: prisma.branchId, organizationId: prisma.organizationId, })); - const oldProjectConfig = renderedOrganizationConfigToProjectCrud(completeConfig); return { id: prisma.id, - /** @deprecated */ - config: oldProjectConfig, - completeConfig, + config: completeConfig, branchId: prisma.branchId, organization: prisma.organizationId === null ? null : { // TODO actual organization type diff --git a/apps/backend/src/oauth/index.tsx b/apps/backend/src/oauth/index.tsx index 20d14f094..d7165a2c1 100644 --- a/apps/backend/src/oauth/index.tsx +++ b/apps/backend/src/oauth/index.tsx @@ -1,7 +1,6 @@ -import { DEFAULT_BRANCH_ID } from "@/lib/tenancies"; +import { DEFAULT_BRANCH_ID, Tenancy } from "@/lib/tenancies"; import { DiscordProvider } from "@/oauth/providers/discord"; import OAuth2Server from "@node-oauth/oauth2-server"; -import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { OAuthModel } from "./model"; @@ -16,8 +15,8 @@ import { LinkedInProvider } from "./providers/linkedin"; import { MicrosoftProvider } from "./providers/microsoft"; import { MockProvider } from "./providers/mock"; import { SpotifyProvider } from "./providers/spotify"; -import { XProvider } from "./providers/x"; import { TwitchProvider } from "./providers/twitch"; +import { XProvider } from "./providers/x"; const _providers = { github: GithubProvider, @@ -57,27 +56,28 @@ export function getProjectBranchFromClientId(clientId: string): [projectId: stri return [projectId, branchId]; } -export async function getProvider(provider: ProjectsCrud['Admin']['Read']['config']['oauth_providers'][number]): Promise { - if (provider.type === 'shared') { - const clientId = _getEnvForProvider(provider.id).clientId; - const clientSecret = _getEnvForProvider(provider.id).clientSecret; +export async function getProvider(provider: Tenancy['config']['auth']['oauth']['providers'][string]): Promise { + const providerType = provider.type || throwErr("Provider type is required for shared providers"); + if (provider.isShared) { + const clientId = _getEnvForProvider(providerType).clientId; + const clientSecret = _getEnvForProvider(providerType).clientSecret; if (clientId === "MOCK") { if (clientSecret !== "MOCK") { throw new StackAssertionError("If OAuth provider client ID is set to MOCK, then client secret must also be set to MOCK"); } - return await mockProvider.create(provider.id); + return await mockProvider.create(providerType); } else { - return await _providers[provider.id].create({ + return await _providers[providerType].create({ clientId, clientSecret, }); } } else { - return await _providers[provider.id].create({ - clientId: provider.client_id || throwErr("Client ID is required for standard providers"), - clientSecret: provider.client_secret || throwErr("Client secret is required for standard providers"), - facebookConfigId: provider.facebook_config_id, - microsoftTenantId: provider.microsoft_tenant_id, + return await _providers[providerType].create({ + clientId: provider.clientId || throwErr("Client ID is required for standard providers"), + clientSecret: provider.clientSecret || throwErr("Client secret is required for standard providers"), + facebookConfigId: provider.facebookConfigId, + microsoftTenantId: provider.microsoftTenantId, }); } } diff --git a/packages/stack-shared/src/interface/crud/environment-config.ts b/packages/stack-shared/src/interface/crud/environment-config.ts new file mode 100644 index 000000000..f44fc53d0 --- /dev/null +++ b/packages/stack-shared/src/interface/crud/environment-config.ts @@ -0,0 +1,33 @@ +import { CrudTypeOf, createCrud } from "../../crud"; +import * as schemaFields from "../../schema-fields"; +import { yupObject } from "../../schema-fields"; + +export const environmentConfigCrudAdminReadSchema = yupObject({ + project_id: schemaFields.yupString().defined(), + branch_id: schemaFields.yupString().defined(), + organization_id: schemaFields.yupString().optional(), + id: schemaFields.yupString().defined(), + config: schemaFields.yupMixed().defined(), +}).defined(); + +export const environmentConfigCrudAdminUpdateSchema = yupObject({ + config: schemaFields.yupMixed().optional(), +}).defined(); + +export const environmentConfigCrud = createCrud({ + adminReadSchema: environmentConfigCrudAdminReadSchema, + adminUpdateSchema: environmentConfigCrudAdminUpdateSchema, + docs: { + adminRead: { + summary: 'Get the current environment config', + description: 'Get the current environment config', + tags: ['Environment Config'], + }, + adminUpdate: { + summary: 'Update the current environment config', + description: 'Update the current environment config', + tags: ['Environment Config'], + }, + }, +}); +export type EnvironmentConfigCrud = CrudTypeOf;