From c98dbce397caa1c9c55d57cf8add6020850b6422 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 8 Sep 2024 15:07:28 -0700 Subject: [PATCH 01/19] added deprecation --- apps/backend/src/lib/projects.tsx | 4 +--- packages/stack-shared/src/interface/crud/projects.ts | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index c51aaab61..67830f367 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -1,12 +1,10 @@ import { prismaClient } from "@/prisma-client"; import { Prisma } from "@prisma/client"; -import { KnownErrors } from "@stackframe/stack-shared"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { UsersCrud } from "@stackframe/stack-shared/dist/interface/crud/users"; -import { StackAssertionError, captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { typedToLowercase } from "@stackframe/stack-shared/dist/utils/strings"; import { fullPermissionInclude, teamPermissionDefinitionJsonFromDbType, teamPermissionDefinitionJsonFromTeamSystemDbType } from "./permissions"; -import { decodeAccessToken } from "./tokens"; export const fullProjectInclude = { config: { diff --git a/packages/stack-shared/src/interface/crud/projects.ts b/packages/stack-shared/src/interface/crud/projects.ts index d9a77c583..382d77790 100644 --- a/packages/stack-shared/src/interface/crud/projects.ts +++ b/packages/stack-shared/src/interface/crud/projects.ts @@ -66,9 +66,12 @@ export const projectsCrudClientReadSchema = yupObject({ display_name: schemaFields.projectDisplayNameSchema.required(), config: yupObject({ sign_up_enabled: schemaFields.projectSignUpEnabledSchema.required(), + client_team_creation_enabled: schemaFields.projectClientTeamCreationEnabledSchema.required(), + /* @deprecated */ credential_enabled: schemaFields.projectCredentialEnabledSchema.required(), + /* @deprecated */ magic_link_enabled: schemaFields.projectMagicLinkEnabledSchema.required(), - client_team_creation_enabled: schemaFields.projectClientTeamCreationEnabledSchema.required(), + /* @deprecated */ enabled_oauth_providers: yupArray(enabledOAuthProviderSchema.required()).required(), }).required(), }).required(); From 060cf8d82f270b5c3720f8b5671cdde010db3da2 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 8 Sep 2024 16:56:36 -0700 Subject: [PATCH 02/19] updated project schema --- .../src/app/api/v1/internal/projects/crud.tsx | 1 - apps/backend/src/lib/projects.tsx | 102 ++++++++++-------- .../[projectId]/auth-methods/providers.tsx | 4 +- .../src/interface/crud/projects.ts | 96 +++++++++++++---- .../src/components-page/account-settings.tsx | 10 +- 5 files changed, 138 insertions(+), 75 deletions(-) diff --git a/apps/backend/src/app/api/v1/internal/projects/crud.tsx b/apps/backend/src/app/api/v1/internal/projects/crud.tsx index 83c55cb42..28c181ef8 100644 --- a/apps/backend/src/app/api/v1/internal/projects/crud.tsx +++ b/apps/backend/src/app/api/v1/internal/projects/crud.tsx @@ -2,7 +2,6 @@ import { fullProjectInclude, listManagedProjectIds, projectPrismaToCrud } from " import { ensureSharedProvider, ensureStandardProvider } from "@/lib/request-checks"; import { prismaClient } from "@/prisma-client"; import { createCrudHandlers } from "@/route-handlers/crud-handler"; -import { ContactChannelType } from "@prisma/client/edge"; import { KnownErrors } from "@stackframe/stack-shared"; import { internalProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; import { projectIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields"; diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index 67830f367..46bab2b8a 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -77,7 +77,8 @@ export type ProjectDB = Prisma.ProjectGetPayload<{ include: typeof fullProjectIn export function projectPrismaToCrud( prisma: Prisma.ProjectGetPayload<{ include: typeof fullProjectInclude }> ): ProjectsCrud["Admin"]["Read"] { - const oauthProviders = prisma.config.authMethodConfigs + /* @deprecated */ + const enabledOauthProviders = prisma.config.authMethodConfigs .map((config) => { if (config.oauthProviderConfig) { const providerConfig = config.oauthProviderConfig; @@ -105,6 +106,50 @@ export function projectPrismaToCrud( .filter((provider): provider is Exclude => !!provider) .sort((a, b) => a.id.localeCompare(b.id)); + const emailConfig = (() => { + const emailServiceConfig = prisma.config.emailServiceConfig; + if (!emailServiceConfig) { + throw new StackAssertionError(`Email service config should be set on project '${prisma.id}'`, { prisma }); + } + if (emailServiceConfig.proxiedEmailServiceConfig) { + return { + type: "shared" + } as const; + } else if (emailServiceConfig.standardEmailServiceConfig) { + const standardEmailConfig = emailServiceConfig.standardEmailServiceConfig; + return { + type: "standard", + host: standardEmailConfig.host, + port: standardEmailConfig.port, + username: standardEmailConfig.username, + password: standardEmailConfig.password, + sender_email: standardEmailConfig.senderEmail, + sender_name: standardEmailConfig.senderName, + } as const; + } else { + throw new StackAssertionError(`Exactly one of the email service configs should be set on project '${prisma.id}'`, { prisma }); + } + })(); + + const domains = prisma.config.domains + .map((domain) => ({ + domain: domain.domain, + handler_path: domain.handlerPath, + })) + .sort((a, b) => a.domain.localeCompare(b.domain)); + + const teamCreatorDefaultPermissions = prisma.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission) + .map(teamPermissionDefinitionJsonFromDbType) + .concat(prisma.config.teamCreateDefaultSystemPermissions.map(teamPermissionDefinitionJsonFromTeamSystemDbType)) + .sort((a, b) => a.id.localeCompare(b.id)) + .map(perm => ({ id: perm.id })); + + const teamMemberDefaultPermissions = prisma.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission) + .map(teamPermissionDefinitionJsonFromDbType) + .concat(prisma.config.teamMemberDefaultSystemPermissions.map(teamPermissionDefinitionJsonFromTeamSystemDbType)) + .sort((a, b) => a.id.localeCompare(b.id)) + .map(perm => ({ id: perm.id })); + const passwordAuth = prisma.config.authMethodConfigs.find((config) => config.passwordConfig); const otpAuth = prisma.config.authMethodConfigs.find((config) => config.otpConfig); @@ -119,52 +164,19 @@ export function projectPrismaToCrud( id: prisma.config.id, allow_localhost: prisma.config.allowLocalhost, sign_up_enabled: prisma.config.signUpEnabled, - credential_enabled: !!passwordAuth, - magic_link_enabled: !!otpAuth, create_team_on_sign_up: prisma.config.createTeamOnSignUp, client_team_creation_enabled: prisma.config.clientTeamCreationEnabled, - domains: prisma.config.domains - .map((domain) => ({ - domain: domain.domain, - handler_path: domain.handlerPath, - })) - .sort((a, b) => a.domain.localeCompare(b.domain)), - oauth_providers: oauthProviders, - enabled_oauth_providers: oauthProviders.filter(provider => provider.enabled), - email_config: (() => { - const emailServiceConfig = prisma.config.emailServiceConfig; - if (!emailServiceConfig) { - throw new StackAssertionError(`Email service config should be set on project '${prisma.id}'`, { prisma }); - } - if (emailServiceConfig.proxiedEmailServiceConfig) { - return { - type: "shared" - } as const; - } else if (emailServiceConfig.standardEmailServiceConfig) { - const standardEmailConfig = emailServiceConfig.standardEmailServiceConfig; - return { - type: "standard", - host: standardEmailConfig.host, - port: standardEmailConfig.port, - username: standardEmailConfig.username, - password: standardEmailConfig.password, - sender_email: standardEmailConfig.senderEmail, - sender_name: standardEmailConfig.senderName, - } as const; - } else { - throw new StackAssertionError(`Exactly one of the email service configs should be set on project '${prisma.id}'`, { prisma }); - } - })(), - team_creator_default_permissions: prisma.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission) - .map(teamPermissionDefinitionJsonFromDbType) - .concat(prisma.config.teamCreateDefaultSystemPermissions.map(teamPermissionDefinitionJsonFromTeamSystemDbType)) - .sort((a, b) => a.id.localeCompare(b.id)) - .map(perm => ({ id: perm.id })), - team_member_default_permissions: prisma.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission) - .map(teamPermissionDefinitionJsonFromDbType) - .concat(prisma.config.teamMemberDefaultSystemPermissions.map(teamPermissionDefinitionJsonFromTeamSystemDbType)) - .sort((a, b) => a.id.localeCompare(b.id)) - .map(perm => ({ id: perm.id })), + team_creator_default_permissions: teamCreatorDefaultPermissions, + team_member_default_permissions: teamMemberDefaultPermissions, + domains: domains, + email_config: emailConfig, + + /* @deprecated */ + enabled_oauth_providers: enabledOauthProviders, + /* @deprecated */ + credential_enabled: !!passwordAuth, + /* @deprecated */ + magic_link_enabled: !!otpAuth, } }; } diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx index e83754856..29b19b84d 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/providers.tsx @@ -181,7 +181,7 @@ export function TurnOffProviderDialog(props: { export function ProviderSettingSwitch(props: Props) { const enabled = !!props.provider?.enabled; - const isShared = props.provider?.type === 'shared'; + const shared = props.provider?.type === 'shared'; const [TurnOffProviderDialogOpen, setTurnOffProviderDialogOpen] = useState(false); const [ProviderSettingDialogOpen, setProviderSettingDialogOpen] = useState(false); @@ -200,7 +200,7 @@ export function ProviderSettingSwitch(props: Props) { label={
{toTitle(props.id)} - {isShared && enabled && + {shared && enabled && Shared keys diff --git a/packages/stack-shared/src/interface/crud/projects.ts b/packages/stack-shared/src/interface/crud/projects.ts index 382d77790..28c5f15f1 100644 --- a/packages/stack-shared/src/interface/crud/projects.ts +++ b/packages/stack-shared/src/interface/crud/projects.ts @@ -6,16 +6,55 @@ const teamPermissionSchema = yupObject({ id: yupString().required(), }).required(); -const oauthProviderSchema = yupObject({ - id: schemaFields.oauthIdSchema.required(), - enabled: schemaFields.oauthEnabledSchema.required(), - type: schemaFields.oauthTypeSchema.required(), - client_id: yupRequiredWhen(schemaFields.oauthClientIdSchema, 'type', 'standard'), - client_secret: yupRequiredWhen(schemaFields.oauthClientSecretSchema, 'type', 'standard'), - - // extra params - facebook_config_id: yupString().optional().meta({ openapiField: { description: 'This parameter is the configuration id for Facebook business login (for things like ads and marketing).' } }), - microsoft_tenant_id: yupString().optional().meta({ openapiField: { description: 'This parameter is the Microsoft tenant id for Microsoft directory' } }), +const oauthProviderConfigSharedFields = { + id: yupString().required(), + type: yupString().oneOf(['password', 'otp', 'oauth']).required(), + enabled: schemaFields.yupBoolean().required(), +}; +const oauthProviderConfigSchema = schemaFields.yupUnion( + yupObject({ + ...oauthProviderConfigSharedFields, + shared: schemaFields.yupBoolean().oneOf([true]).required(), + }).required(), + yupObject({ + ...oauthProviderConfigSharedFields, + shared: schemaFields.yupBoolean().oneOf([false]).required(), + client_id: yupString().required(), + client_secret: yupString().required(), + facebook_config_id: yupString().optional().meta({ openapiField: { description: 'This parameter is the configuration id for Facebook business login (for things like ads and marketing).' } }), + microsoft_tenant_id: yupString().optional().meta({ openapiField: { description: 'This parameter is the Microsoft tenant id for Microsoft directory' } }), + }).required(), +); + +const authMethodSharedFields = { + id: yupString().required(), + enabled: schemaFields.yupBoolean().required(), +}; +const authMethodConfigSchema = schemaFields.yupUnion( + yupObject({ + ...authMethodSharedFields, + type: yupString().oneOf(['password']).required(), + identifier: yupString().required(), + }).required(), + yupObject({ + ...authMethodSharedFields, + type: yupString().oneOf(['otp']).required(), + contact_channel: yupObject({ + type: yupString().oneOf(['email']).required(), + email: yupString().required(), + }).required(), + }).required(), + yupObject({ + ...authMethodSharedFields, + type: yupString().oneOf(['oauth']).required(), + provider_id: yupString().required(), + }).required(), +); + +const connectedAccountSchema = yupObject({ + id: yupString().required(), + enabled: schemaFields.yupBoolean().required(), + provider_id: yupString().required(), }); const enabledOAuthProviderSchema = yupObject({ @@ -48,16 +87,27 @@ export const projectsCrudAdminReadSchema = yupObject({ id: schemaFields.projectConfigIdSchema.required(), allow_localhost: schemaFields.projectAllowLocalhostSchema.required(), sign_up_enabled: schemaFields.projectSignUpEnabledSchema.required(), - credential_enabled: schemaFields.projectCredentialEnabledSchema.required(), - magic_link_enabled: schemaFields.projectMagicLinkEnabledSchema.required(), client_team_creation_enabled: schemaFields.projectClientTeamCreationEnabledSchema.required(), - oauth_providers: yupArray(oauthProviderSchema.required()).required(), - enabled_oauth_providers: yupArray(enabledOAuthProviderSchema.required()).required(), - domains: yupArray(domainSchema.required()).required(), - email_config: emailConfigSchema.required(), create_team_on_sign_up: schemaFields.projectCreateTeamOnSignUpSchema.required(), + team_creator_default_permissions: yupArray(teamPermissionSchema.required()).required(), team_member_default_permissions: yupArray(teamPermissionSchema.required()).required(), + + domains: yupArray(domainSchema.required()).required(), + email_config: emailConfigSchema.required(), + + oauth_provider_configs: yupArray(oauthProviderConfigSchema).required(), + auth_method_configs: yupArray(authMethodConfigSchema).required(), + connected_accounts: yupArray(connectedAccountSchema).required(), + + // ============= + /* @deprecated */ + credential_enabled: schemaFields.projectCredentialEnabledSchema.required(), + /* @deprecated */ + magic_link_enabled: schemaFields.projectMagicLinkEnabledSchema.required(), + /* @deprecated */ + enabled_oauth_providers: yupArray(enabledOAuthProviderSchema.required()).required(), + // ============= }).required(), }).required(); @@ -66,6 +116,8 @@ export const projectsCrudClientReadSchema = yupObject({ display_name: schemaFields.projectDisplayNameSchema.required(), config: yupObject({ sign_up_enabled: schemaFields.projectSignUpEnabledSchema.required(), + + // ============== client_team_creation_enabled: schemaFields.projectClientTeamCreationEnabledSchema.required(), /* @deprecated */ credential_enabled: schemaFields.projectCredentialEnabledSchema.required(), @@ -73,6 +125,7 @@ export const projectsCrudClientReadSchema = yupObject({ magic_link_enabled: schemaFields.projectMagicLinkEnabledSchema.required(), /* @deprecated */ enabled_oauth_providers: yupArray(enabledOAuthProviderSchema.required()).required(), + // =============== }).required(), }).required(); @@ -82,17 +135,16 @@ export const projectsCrudAdminUpdateSchema = yupObject({ description: schemaFields.projectDescriptionSchema.optional(), is_production_mode: schemaFields.projectIsProductionModeSchema.optional(), config: yupObject({ + allow_localhost: schemaFields.projectAllowLocalhostSchema.optional(), sign_up_enabled: schemaFields.projectSignUpEnabledSchema.optional(), - credential_enabled: schemaFields.projectCredentialEnabledSchema.optional(), - magic_link_enabled: schemaFields.projectMagicLinkEnabledSchema.optional(), client_team_creation_enabled: schemaFields.projectClientTeamCreationEnabledSchema.optional(), - allow_localhost: schemaFields.projectAllowLocalhostSchema.optional(), - email_config: emailConfigSchema.optional().default(undefined), - domains: yupArray(domainSchema.required()).optional().default(undefined), - oauth_providers: yupArray(oauthProviderSchema.required()).optional().default(undefined), create_team_on_sign_up: schemaFields.projectCreateTeamOnSignUpSchema.optional(), + team_creator_default_permissions: yupArray(teamPermissionSchema.required()).optional(), team_member_default_permissions: yupArray(teamPermissionSchema.required()).optional(), + + domains: yupArray(domainSchema.required()).optional().default(undefined), + email_config: emailConfigSchema.optional().default(undefined), }).optional().default(undefined), }).required(); diff --git a/packages/stack/src/components-page/account-settings.tsx b/packages/stack/src/components-page/account-settings.tsx index d34422b53..918de4eb2 100644 --- a/packages/stack/src/components-page/account-settings.tsx +++ b/packages/stack/src/components-page/account-settings.tsx @@ -290,7 +290,7 @@ function MfaSection() { const [qrCodeUrl, setQrCodeUrl] = useState(null); const [mfaCode, setMfaCode] = useState(""); const [isMaybeWrong, setIsMaybeWrong] = useState(false); - const isEnabled = user.isMultiFactorRequired; + const enabled = user.isMultiFactorRequired; const [handleSubmit, isLoading] = useAsyncCallback(async () => { await user.update({ @@ -317,7 +317,7 @@ function MfaSection() {
- {isEnabled ? ( + {enabled ? ( Multi-factor authentication is currently enabled. ) : ( generatedSecret ? ( @@ -346,9 +346,9 @@ function MfaSection() {
From 0c4194b4546930dd1207dd1acf722fd7f1780c63 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sun, 8 Sep 2024 17:13:21 -0700 Subject: [PATCH 03/19] improved code structure --- apps/backend/src/lib/projects.tsx | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index 46bab2b8a..3fcb30846 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -104,6 +104,7 @@ export function projectPrismaToCrud( } }) .filter((provider): provider is Exclude => !!provider) + .filter(provider => provider.enabled) .sort((a, b) => a.id.localeCompare(b.id)); const emailConfig = (() => { @@ -138,17 +139,13 @@ export function projectPrismaToCrud( })) .sort((a, b) => a.domain.localeCompare(b.domain)); - const teamCreatorDefaultPermissions = prisma.config.permissions.filter(perm => perm.isDefaultTeamCreatorPermission) - .map(teamPermissionDefinitionJsonFromDbType) - .concat(prisma.config.teamCreateDefaultSystemPermissions.map(teamPermissionDefinitionJsonFromTeamSystemDbType)) - .sort((a, b) => a.id.localeCompare(b.id)) - .map(perm => ({ id: perm.id })); - - const teamMemberDefaultPermissions = prisma.config.permissions.filter(perm => perm.isDefaultTeamMemberPermission) - .map(teamPermissionDefinitionJsonFromDbType) - .concat(prisma.config.teamMemberDefaultSystemPermissions.map(teamPermissionDefinitionJsonFromTeamSystemDbType)) - .sort((a, b) => a.id.localeCompare(b.id)) - .map(perm => ({ id: perm.id })); + const getPermissions = (type: 'creator' | 'member') => { + return prisma.config.permissions.filter(perm => type === 'creator' ? perm.isDefaultTeamCreatorPermission : perm.isDefaultTeamMemberPermission) + .map(teamPermissionDefinitionJsonFromDbType) + .concat((type === 'creator' ? prisma.config.teamCreateDefaultSystemPermissions : prisma.config.teamMemberDefaultSystemPermissions ).map(teamPermissionDefinitionJsonFromTeamSystemDbType)) + .map(perm => ({ id: perm.id })) + .sort((a, b) => a.id.localeCompare(b.id)); + }; const passwordAuth = prisma.config.authMethodConfigs.find((config) => config.passwordConfig); const otpAuth = prisma.config.authMethodConfigs.find((config) => config.otpConfig); @@ -166,8 +163,8 @@ export function projectPrismaToCrud( sign_up_enabled: prisma.config.signUpEnabled, create_team_on_sign_up: prisma.config.createTeamOnSignUp, client_team_creation_enabled: prisma.config.clientTeamCreationEnabled, - team_creator_default_permissions: teamCreatorDefaultPermissions, - team_member_default_permissions: teamMemberDefaultPermissions, + team_creator_default_permissions: getPermissions('creator'), + team_member_default_permissions: getPermissions('member'), domains: domains, email_config: emailConfig, From 9190805c7dd94420462192b7b08b161b0249259a Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Wed, 11 Sep 2024 16:26:41 -0700 Subject: [PATCH 04/19] fixed project to crud --- apps/backend/src/lib/projects.tsx | 64 ++++++++++++++++++- .../src/interface/crud/projects.ts | 11 +--- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index 8432ef017..f615f8ded 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -99,7 +99,7 @@ export function projectPrismaToCrud( microsoft_tenant_id: providerConfig.standardOAuthConfig.microsoftTenantId ?? undefined, } as const; } else { - throw new StackAssertionError(`Exactly one of the provider configs should be set on provider config '${config.id}' of project '${prisma.id}'`, { prisma }); + throw new StackAssertionError(`DB union violation: provider config '${config.id}' of project '${prisma.id}' is neither proxied nor standard`, { prisma }); } } }) @@ -107,6 +107,63 @@ export function projectPrismaToCrud( .filter(provider => provider.enabled) .sort((a, b) => a.id.localeCompare(b.id)); + const oauthProviderConfigs = prisma.config.oauthProviderConfigs.map(provider => { + if (provider.proxiedOAuthConfig) { + return { + id: provider.id, + shared: true, + type: typedToLowercase(provider.proxiedOAuthConfig.type), + } as const; + } else if (provider.standardOAuthConfig) { + return { + id: provider.id, + shared: false, + type: typedToLowercase(provider.standardOAuthConfig.type), + client_id: provider.standardOAuthConfig.clientId, + client_secret: provider.standardOAuthConfig.clientSecret, + facebook_config_id: provider.standardOAuthConfig.facebookConfigId ?? undefined, + microsoft_tenant_id: provider.standardOAuthConfig.microsoftTenantId ?? undefined, + } as const; + } else { + throw new StackAssertionError(`DB union violation: provider config '${provider.id}' of project '${prisma.id}' is neither proxied nor standard`, { prisma }); + } + }); + + const authMethodConfigs = prisma.config.authMethodConfigs.map(config => { + if (config.passwordConfig) { + return { + id: config.id, + enabled: config.enabled, + type: "password", + } as const; + } else if (config.otpConfig) { + return { + id: config.id, + enabled: config.enabled, + type: "otp", + } as const; + } else if (config.oauthProviderConfig) { + return { + id: config.id, + type: "oauth", + enabled: config.enabled, + provider_config_id: config.oauthProviderConfig.id, + } as const; + } + throw new StackAssertionError(`DB union violation: auth method config '${config.id}' of project '${prisma.id}' is neither password nor otp`, { prisma }); + }); + + const connectedAccountConfigs = prisma.config.connectedAccountConfigs.map(config => { + if (!config.oauthProviderConfig) { + throw new StackAssertionError(`DB non-nullable violation: connected account config '${config.id}' of project '${prisma.id}' is not connected to an oauth provider`, { prisma }); + } + return { + id: config.id, + enabled: config.enabled, + provider_id: config.oauthProviderConfig.id, + } as const; + }); + const emailConfig = (() => { const emailServiceConfig = prisma.config.emailServiceConfig; if (!emailServiceConfig) { @@ -128,7 +185,7 @@ export function projectPrismaToCrud( sender_name: standardEmailConfig.senderName, } as const; } else { - throw new StackAssertionError(`Exactly one of the email service configs should be set on project '${prisma.id}'`, { prisma }); + throw new StackAssertionError(`DB union violation: email service config '${prisma.id}' of project '${prisma.id}' is neither proxied nor standard`, { prisma }); } })(); @@ -167,6 +224,9 @@ export function projectPrismaToCrud( team_member_default_permissions: getPermissions('member'), domains: domains, email_config: emailConfig, + oauth_provider_configs: oauthProviderConfigs, + auth_method_configs: authMethodConfigs, + connected_accounts: connectedAccountConfigs, /* @deprecated */ enabled_oauth_providers: enabledOauthProviders, diff --git a/packages/stack-shared/src/interface/crud/projects.ts b/packages/stack-shared/src/interface/crud/projects.ts index 28c5f15f1..22ad7147d 100644 --- a/packages/stack-shared/src/interface/crud/projects.ts +++ b/packages/stack-shared/src/interface/crud/projects.ts @@ -1,6 +1,7 @@ import { CrudTypeOf, createCrud } from "../../crud"; import * as schemaFields from "../../schema-fields"; import { yupArray, yupObject, yupRequiredWhen, yupString } from "../../schema-fields"; +import { allProviders } from "../../utils/oauth"; const teamPermissionSchema = yupObject({ id: yupString().required(), @@ -8,8 +9,7 @@ const teamPermissionSchema = yupObject({ const oauthProviderConfigSharedFields = { id: yupString().required(), - type: yupString().oneOf(['password', 'otp', 'oauth']).required(), - enabled: schemaFields.yupBoolean().required(), + type: yupString().oneOf(allProviders).required(), }; const oauthProviderConfigSchema = schemaFields.yupUnion( yupObject({ @@ -34,20 +34,15 @@ const authMethodConfigSchema = schemaFields.yupUnion( yupObject({ ...authMethodSharedFields, type: yupString().oneOf(['password']).required(), - identifier: yupString().required(), }).required(), yupObject({ ...authMethodSharedFields, type: yupString().oneOf(['otp']).required(), - contact_channel: yupObject({ - type: yupString().oneOf(['email']).required(), - email: yupString().required(), - }).required(), }).required(), yupObject({ ...authMethodSharedFields, type: yupString().oneOf(['oauth']).required(), - provider_id: yupString().required(), + provider_config_id: yupString().required(), }).required(), ); From 2448f783231b36e44c34aac9c4af5347d19f413f Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Sat, 14 Sep 2024 11:45:55 -0700 Subject: [PATCH 05/19] wip --- .../src/app/api/v1/internal/projects/crud.tsx | 83 +++++++++++-------- .../src/interface/crud/projects.ts | 4 + 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/apps/backend/src/app/api/v1/internal/projects/crud.tsx b/apps/backend/src/app/api/v1/internal/projects/crud.tsx index 1d91ea719..bdc4fc660 100644 --- a/apps/backend/src/app/api/v1/internal/projects/crud.tsx +++ b/apps/backend/src/app/api/v1/internal/projects/crud.tsx @@ -43,6 +43,8 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand const ownerPack = ownerPacks.find(p => p.has(user.id)); const userIds = ownerPack ? [...ownerPack] : [user.id]; + // TODO: ensure that the oauth configs are valid + const result = await prismaClient.$transaction(async (tx) => { const project = await tx.project.create({ data: { @@ -62,17 +64,17 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand handlerPath: item.handler_path, })) } : undefined, - oauthProviderConfigs: data.config?.oauth_providers ? { - create: data.config.oauth_providers.map(item => ({ + oauthProviderConfigs: data.config?.oauth_provider_configs ? { + create: data.config.oauth_provider_configs.map(item => ({ id: item.id, - proxiedOAuthConfig: item.type === "shared" ? { + proxiedOAuthConfig: item.shared ? { create: { - type: typedToUppercase(ensureSharedProvider(item.id)), + type: typedToUppercase(ensureSharedProvider(item.type)), } } : undefined, - standardOAuthConfig: item.type === "standard" ? { + standardOAuthConfig: !item.shared ? { create: { - type: typedToUppercase(ensureStandardProvider(item.id)), + type: typedToUppercase(ensureStandardProvider(item.type)), clientId: item.client_id ?? throwErr('client_id is required'), clientSecret: item.client_secret ?? throwErr('client_secret is required'), facebookConfigId: item.facebook_config_id, @@ -117,35 +119,44 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand }, data: { authMethodConfigs: { - create: [ - ...data.config?.oauth_providers ? project.config.oauthProviderConfigs.map(item => ({ - enabled: (data.config?.oauth_providers?.find(p => p.id === item.id) ?? throwErr("oauth provider not found")).enabled, - oauthProviderConfig: { - connect: { - projectConfigId_id: { - projectConfigId: project.config.id, - id: item.id, + create: data.config?.auth_method_configs?.map(item => { + switch (item.type) { + case "oauth": { + return { + id: item.id, + enabled: item.enabled, + oauthConfig: { + connect: { + id: item.provider_config_id, + projectConfigId: project.config.id, + } } - } + }; } - })) : [], - ...data.config?.magic_link_enabled ? [{ - enabled: true, - otpConfig: { - create: { - contactChannelType: 'EMAIL', - } - }, - }] : [], - ...(data.config?.credential_enabled ?? true) ? [{ - enabled: true, - passwordConfig: { - create: { - identifierType: 'EMAIL', - } - }, - }] : [], - ] + case "password": { + return { + id: item.id, + enabled: item.enabled, + passwordConfig: { + create: { + identifierType: 'EMAIL', + } + } + }; + } + case "otp": { + return { + id: item.id, + enabled: item.enabled, + otpConfig: { + create: { + contactChannelType: 'EMAIL', + } + } + }; + } + } + }), } } }); @@ -156,9 +167,9 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand id: project.config.id, }, data: { - connectedAccountConfigs: data.config?.oauth_providers ? { - create: project.config.oauthProviderConfigs.map(item => ({ - enabled: (data.config?.oauth_providers?.find(p => p.id === item.id) ?? throwErr("oauth provider not found")).enabled, + connectedAccountConfigs: data.config?.connected_accounts ? { + create: data.config.connected_accounts?.map(item => ({ + enabled: item.enabled, oauthProviderConfig: { connect: { projectConfigId_id: { diff --git a/packages/stack-shared/src/interface/crud/projects.ts b/packages/stack-shared/src/interface/crud/projects.ts index 22ad7147d..d172eb2bb 100644 --- a/packages/stack-shared/src/interface/crud/projects.ts +++ b/packages/stack-shared/src/interface/crud/projects.ts @@ -140,6 +140,10 @@ export const projectsCrudAdminUpdateSchema = yupObject({ domains: yupArray(domainSchema.required()).optional().default(undefined), email_config: emailConfigSchema.optional().default(undefined), + + oauth_provider_configs: yupArray(oauthProviderConfigSchema).optional().default(undefined), + auth_method_configs: yupArray(authMethodConfigSchema).optional().default(undefined), + connected_accounts: yupArray(connectedAccountSchema).optional().default(undefined), }).optional().default(undefined), }).required(); From 84e08fdac919ee8304aae0520c5910875d82a064 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Wed, 18 Sep 2024 13:31:00 -0700 Subject: [PATCH 06/19] removed old code --- .../src/app/api/v1/internal/projects/crud.tsx | 2 +- apps/backend/src/lib/projects.tsx | 11 ++++- .../src/interface/crud/projects.ts | 22 +++++++-- packages/stack/src/lib/stack-app.ts | 47 ------------------- 4 files changed, 30 insertions(+), 52 deletions(-) diff --git a/apps/backend/src/app/api/v1/internal/projects/crud.tsx b/apps/backend/src/app/api/v1/internal/projects/crud.tsx index d8c303afd..86561a0de 100644 --- a/apps/backend/src/app/api/v1/internal/projects/crud.tsx +++ b/apps/backend/src/app/api/v1/internal/projects/crud.tsx @@ -169,7 +169,7 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand }, data: { connectedAccountConfigs: data.config?.connected_accounts ? { - create: data.config.connected_accounts?.map(item => ({ + create: data.config.connected_accounts.map(item => ({ enabled: item.enabled, oauthProviderConfig: { connect: { diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index 965cfab9c..fe3268a9c 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -206,6 +206,10 @@ export function projectPrismaToCrud( const passwordAuth = prisma.config.authMethodConfigs.find((config) => config.passwordConfig && config.enabled); const otpAuth = prisma.config.authMethodConfigs.find((config) => config.otpConfig && config.enabled); + const enabledOAuthProviderIds = new Set( + authMethodConfigs.filter(config => config.enabled).map(config => config.provider_config_id) + .concat(connectedAccountConfigs.filter(config => config.enabled).map(config => config.provider_id)) + ); return { id: prisma.id, @@ -223,11 +227,16 @@ export function projectPrismaToCrud( client_user_deletion_enabled: prisma.config.clientUserDeletionEnabled, team_creator_default_permissions: getPermissions('creator'), team_member_default_permissions: getPermissions('member'), + domains: domains, email_config: emailConfig, + oauth_provider_configs: oauthProviderConfigs, auth_method_configs: authMethodConfigs, - connected_accounts: connectedAccountConfigs, + connected_account_configs: connectedAccountConfigs, + enabled_oauth_provider_configs: oauthProviderConfigs.filter(provider => enabledOAuthProviderIds.has(provider.id)), + enabled_auth_method_configs: authMethodConfigs.filter(config => config.enabled), + enabled_connected_accounts_configs: connectedAccountConfigs.filter(config => config.enabled), /* @deprecated */ enabled_oauth_providers: enabledOauthProviders, diff --git a/packages/stack-shared/src/interface/crud/projects.ts b/packages/stack-shared/src/interface/crud/projects.ts index 5e8e70350..f7c946dd0 100644 --- a/packages/stack-shared/src/interface/crud/projects.ts +++ b/packages/stack-shared/src/interface/crud/projects.ts @@ -26,6 +26,11 @@ const oauthProviderConfigSchema = schemaFields.yupUnion( }).required(), ); +const clientOAuthProviderConfigSchema = yupObject({ + id: yupString().required(), + type: yupString().oneOf(allProviders).required(), +}).required(); + const authMethodSharedFields = { id: yupString().required(), enabled: schemaFields.yupBoolean().required(), @@ -46,12 +51,16 @@ const authMethodConfigSchema = schemaFields.yupUnion( }).required(), ); -const connectedAccountSchema = yupObject({ +const clientAuthMethodConfigSchema = authMethodConfigSchema; + +const connectedAccountConfigSchema = yupObject({ id: yupString().required(), enabled: schemaFields.yupBoolean().required(), provider_id: yupString().required(), }); +const clientConnectedAccountConfigSchema = connectedAccountConfigSchema; + const enabledOAuthProviderSchema = yupObject({ id: schemaFields.oauthIdSchema.required(), }); @@ -94,7 +103,10 @@ export const projectsCrudAdminReadSchema = yupObject({ oauth_provider_configs: yupArray(oauthProviderConfigSchema).required(), auth_method_configs: yupArray(authMethodConfigSchema).required(), - connected_accounts: yupArray(connectedAccountSchema).required(), + connected_account_configs: yupArray(connectedAccountConfigSchema).required(), + enabled_oauth_provider_configs: yupArray(clientOAuthProviderConfigSchema).required(), + enabled_auth_method_configs: yupArray(clientAuthMethodConfigSchema).required(), + enabled_connected_accounts_configs: yupArray(clientConnectedAccountConfigSchema).required(), // ============= /* @deprecated */ @@ -116,6 +128,10 @@ export const projectsCrudClientReadSchema = yupObject({ client_team_creation_enabled: schemaFields.projectClientTeamCreationEnabledSchema.required(), client_user_deletion_enabled: schemaFields.projectClientUserDeletionEnabledSchema.required(), + enabled_oauth_provider_configs: yupArray(clientOAuthProviderConfigSchema).required(), + enabled_auth_method_configs: yupArray(clientAuthMethodConfigSchema).required(), + enabled_connected_accounts_configs: yupArray(clientConnectedAccountConfigSchema).required(), + // ============== /* @deprecated */ credential_enabled: schemaFields.projectCredentialEnabledSchema.required(), @@ -147,7 +163,7 @@ export const projectsCrudAdminUpdateSchema = yupObject({ oauth_provider_configs: yupArray(oauthProviderConfigSchema).optional().default(undefined), auth_method_configs: yupArray(authMethodConfigSchema).optional().default(undefined), - connected_accounts: yupArray(connectedAccountSchema).optional().default(undefined), + connected_accounts: yupArray(connectedAccountConfigSchema).optional().default(undefined), }).optional().default(undefined), }).required(); diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 4bb46aeb6..51897dbf3 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -692,13 +692,8 @@ class _StackClientAppImpl ({ - id: p.id, - })), } }; } @@ -791,7 +786,6 @@ class _StackClientAppImpl ((p.type === 'shared' ? { - id: p.id, - enabled: p.enabled, - type: 'shared', - } as const : { - id: p.id, - enabled: p.enabled, - type: 'standard', - clientId: p.client_id ?? throwErr("Client ID is missing"), - clientSecret: p.client_secret ?? throwErr("Client secret is missing"), - facebookConfigId: p.facebook_config_id, - microsoftTenantId: p.microsoft_tenant_id, - } as const))), emailConfig: data.config.email_config.type === 'shared' ? { type: 'shared' } : { @@ -2336,10 +2315,6 @@ type BaseUser = { * Whether the user has a password set. */ readonly hasPassword: boolean, - /** - * @deprecated - */ - readonly oauthProviders: readonly { id: string }[], readonly isMultiFactorRequired: boolean, @@ -2537,17 +2512,6 @@ function adminProjectUpdateOptionsToCrud(options: AdminProjectUpdateOptions): Pr domain: d.domain, handler_path: d.handlerPath })), - oauth_providers: options.config?.oauthProviders?.map((p) => ({ - id: p.id as any, - enabled: p.enabled, - type: p.type, - ...(p.type === 'standard' && { - client_id: p.clientId, - client_secret: p.clientSecret, - facebook_config_id: p.facebookConfigId, - microsoft_tenant_id: p.microsoftTenantId, - }), - })), email_config: options.config?.emailConfig && ( options.config.emailConfig.type === 'shared' ? { type: 'shared', @@ -2562,8 +2526,6 @@ function adminProjectUpdateOptionsToCrud(options: AdminProjectUpdateOptions): Pr } ), sign_up_enabled: options.config?.signUpEnabled, - credential_enabled: options.config?.credentialEnabled, - magic_link_enabled: options.config?.magicLinkEnabled, allow_localhost: options.config?.allowLocalhost, create_team_on_sign_up: options.config?.createTeamOnSignUp, client_team_creation_enabled: options.config?.clientTeamCreationEnabled, @@ -2588,11 +2550,8 @@ type _______________PROJECT_CONFIG_______________ = never; // this is a marker export type ProjectConfig = { readonly signUpEnabled: boolean, - readonly credentialEnabled: boolean, - readonly magicLinkEnabled: boolean, readonly clientTeamCreationEnabled: boolean, readonly clientUserDeletionEnabled: boolean, - readonly oauthProviders: OAuthProviderConfig[], }; export type OAuthProviderConfig = { @@ -2602,12 +2561,9 @@ export type OAuthProviderConfig = { export type AdminProjectConfig = { readonly id: string, readonly signUpEnabled: boolean, - readonly credentialEnabled: boolean, - readonly magicLinkEnabled: boolean, readonly clientTeamCreationEnabled: boolean, readonly clientUserDeletionEnabled: boolean, readonly allowLocalhost: boolean, - readonly oauthProviders: AdminOAuthProviderConfig[], readonly emailConfig?: AdminEmailConfig, readonly domains: AdminDomainConfig[], readonly createTeamOnSignUp: boolean, @@ -2654,10 +2610,7 @@ export type AdminProjectConfigUpdateOptions = { domain: string, handlerPath: string, }[], - oauthProviders?: AdminOAuthProviderConfig[], signUpEnabled?: boolean, - credentialEnabled?: boolean, - magicLinkEnabled?: boolean, clientTeamCreationEnabled?: boolean, clientUserDeletionEnabled?: boolean, allowLocalhost?: boolean, From fe031146326ec7343ee5adbac04f9730478601e0 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Wed, 18 Sep 2024 14:24:06 -0700 Subject: [PATCH 07/19] updated stack app --- apps/backend/src/lib/projects.tsx | 2 +- .../src/interface/crud/projects.ts | 4 +- packages/stack/src/lib/stack-app.ts | 122 +++++++++++++++++- 3 files changed, 122 insertions(+), 6 deletions(-) diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index fe3268a9c..96e748229 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -236,7 +236,7 @@ export function projectPrismaToCrud( connected_account_configs: connectedAccountConfigs, enabled_oauth_provider_configs: oauthProviderConfigs.filter(provider => enabledOAuthProviderIds.has(provider.id)), enabled_auth_method_configs: authMethodConfigs.filter(config => config.enabled), - enabled_connected_accounts_configs: connectedAccountConfigs.filter(config => config.enabled), + enabled_connected_account_configs: connectedAccountConfigs.filter(config => config.enabled), /* @deprecated */ enabled_oauth_providers: enabledOauthProviders, diff --git a/packages/stack-shared/src/interface/crud/projects.ts b/packages/stack-shared/src/interface/crud/projects.ts index f7c946dd0..d31b02b84 100644 --- a/packages/stack-shared/src/interface/crud/projects.ts +++ b/packages/stack-shared/src/interface/crud/projects.ts @@ -106,7 +106,7 @@ export const projectsCrudAdminReadSchema = yupObject({ connected_account_configs: yupArray(connectedAccountConfigSchema).required(), enabled_oauth_provider_configs: yupArray(clientOAuthProviderConfigSchema).required(), enabled_auth_method_configs: yupArray(clientAuthMethodConfigSchema).required(), - enabled_connected_accounts_configs: yupArray(clientConnectedAccountConfigSchema).required(), + enabled_connected_account_configs: yupArray(clientConnectedAccountConfigSchema).required(), // ============= /* @deprecated */ @@ -130,7 +130,7 @@ export const projectsCrudClientReadSchema = yupObject({ enabled_oauth_provider_configs: yupArray(clientOAuthProviderConfigSchema).required(), enabled_auth_method_configs: yupArray(clientAuthMethodConfigSchema).required(), - enabled_connected_accounts_configs: yupArray(clientConnectedAccountConfigSchema).required(), + enabled_connected_account_configs: yupArray(clientConnectedAccountConfigSchema).required(), // ============== /* @deprecated */ diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 51897dbf3..22f171c4e 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -694,6 +694,40 @@ class _StackClientAppImpl ({ + id: config.id, + type: config.type, + })), + enabledAuthMethodConfigs: crud.config.enabled_auth_method_configs.map((config) => { + switch (config.type) { + case 'password': { + return { + id: config.id, + type: 'password', + enabled: config.enabled, + }; + } + case 'otp': { + return { + id: config.id, + type: 'otp', + enabled: config.enabled, + }; + } + case 'oauth': { + return { + id: config.id, + type: 'oauth', + enabled: config.enabled, + providerConfigId: config.provider_config_id, + }; + } + } + }), + enabledConnectedAccountConfigs: crud.config.enabled_connected_account_configs.map((config) => ({ + id: config.id, + providerId: config.provider_id, + })), } }; } @@ -1978,6 +2012,7 @@ class _StackAdminAppImpl { + if (p.shared) { + return { + id: p.id, + type: p.type, + shared: p.shared, + }; + } else { + return { + id: p.id, + type: p.type, + clientId: p.client_id, + clientSecret: p.client_secret, + facebookConfigId: p.facebook_config_id, + microsoftTenantId: p.microsoft_tenant_id, + }; + } + }), + authMethodConfigs: data.config.auth_method_configs.map((p) => { + switch (p.type) { + case 'password': { + return { + id: p.id, + enabled: p.enabled, + type: p.type, + }; + } + case 'otp': { + return { + id: p.id, + enabled: p.enabled, + type: p.type, + }; + } + case 'oauth': { + return { + id: p.id, + enabled: p.enabled, + type: p.type, + providerConfigId: p.provider_config_id, + }; + } + } + }), + connectedAccountConfigs: data.config.connected_account_configs.map((c) => ({ + id: c.id, + enabled: c.enabled, + providerId: c.provider_id, + })), }, async update(update: AdminProjectUpdateOptions) { @@ -2552,12 +2641,28 @@ export type ProjectConfig = { readonly signUpEnabled: boolean, readonly clientTeamCreationEnabled: boolean, readonly clientUserDeletionEnabled: boolean, + readonly enabledOAuthProviderConfigs: OAuthProviderConfig[], + readonly enabledAuthMethodConfigs: AuthMethodConfig[], + readonly enabledConnectedAccountConfigs: ConnectedAccountConfig[], }; export type OAuthProviderConfig = { readonly id: string, }; +export type AuthMethodConfig = { + readonly id: string, +} & ( + | { type: 'oauth', providerConfigId: string } + | { type: 'password' } + | { type: 'otp' } +); + +export type ConnectedAccountConfig = { + readonly id: string, + readonly providerId: string, +} + export type AdminProjectConfig = { readonly id: string, readonly signUpEnabled: boolean, @@ -2569,6 +2674,9 @@ export type AdminProjectConfig = { readonly createTeamOnSignUp: boolean, readonly teamCreatorDefaultPermissions: AdminTeamPermission[], readonly teamMemberDefaultPermissions: AdminTeamPermission[], + readonly oauthProviderConfigs: AdminOAuthProviderConfig[], + readonly authMethodConfigs: AdminAuthMethodConfig[], + readonly connectedAccountConfigs: AdminConnectedAccountConfig[], }; export type AdminEmailConfig = ( @@ -2593,11 +2701,11 @@ export type AdminDomainConfig = { export type AdminOAuthProviderConfig = { id: string, - enabled: boolean, } & ( - | { type: 'shared' } + | { shared: true } | { - type: 'standard', + shared: false, + type: string, clientId: string, clientSecret: string, facebookConfigId?: string, @@ -2605,6 +2713,14 @@ export type AdminOAuthProviderConfig = { } ) & OAuthProviderConfig; +export type AdminAuthMethodConfig = { + enabled: boolean, +} & AuthMethodConfig; + +export type AdminConnectedAccountConfig = { + enabled: boolean, +} & ConnectedAccountConfig; + export type AdminProjectConfigUpdateOptions = { domains?: { domain: string, From b72a0c7f373ebc286db8079e4930a51d15f8353e Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Wed, 18 Sep 2024 14:57:39 -0700 Subject: [PATCH 08/19] fixed boolean type --- packages/stack-shared/src/interface/crud/projects.ts | 4 ++-- packages/stack/src/lib/stack-app.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/stack-shared/src/interface/crud/projects.ts b/packages/stack-shared/src/interface/crud/projects.ts index d31b02b84..a9a25189e 100644 --- a/packages/stack-shared/src/interface/crud/projects.ts +++ b/packages/stack-shared/src/interface/crud/projects.ts @@ -14,11 +14,11 @@ const oauthProviderConfigSharedFields = { const oauthProviderConfigSchema = schemaFields.yupUnion( yupObject({ ...oauthProviderConfigSharedFields, - shared: schemaFields.yupBoolean().oneOf([true]).required(), + shared: schemaFields.yupBoolean().isTrue().required(), }).required(), yupObject({ ...oauthProviderConfigSharedFields, - shared: schemaFields.yupBoolean().oneOf([false]).required(), + shared: schemaFields.yupBoolean().isFalse().required(), client_id: yupString().required(), client_secret: yupString().required(), facebook_config_id: yupString().optional().meta({ openapiField: { description: 'This parameter is the configuration id for Facebook business login (for things like ads and marketing).' } }), diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 22f171c4e..278c8e3e1 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -2062,6 +2062,7 @@ class _StackAdminAppImpl Date: Wed, 18 Sep 2024 17:10:23 -0700 Subject: [PATCH 09/19] fixed more types --- .../stack/src/components-page/auth-page.tsx | 35 +++++++++++-------- .../src/components/oauth-button-group.tsx | 17 +++++---- .../stack/src/components/oauth-button.tsx | 6 ++++ packages/stack/src/lib/stack-app.ts | 1 + 4 files changed, 35 insertions(+), 24 deletions(-) diff --git a/packages/stack/src/components-page/auth-page.tsx b/packages/stack/src/components-page/auth-page.tsx index d8196aa28..45a48225e 100644 --- a/packages/stack/src/components-page/auth-page.tsx +++ b/packages/stack/src/components-page/auth-page.tsx @@ -13,21 +13,24 @@ import { PredefinedMessageCard } from '../components/message-cards/predefined-me import { OAuthButtonGroup } from '../components/oauth-button-group'; import { useTranslation } from '../lib/translations'; +export type MockProject = { + config: { + signUpEnabled: boolean, + enabledAuthMethodConfigs: ({ + type: 'password' | 'otp', + } | { + type: 'oauth', + provider_config_id: string, + })[], + }, +}; + export function AuthPage(props: { fullPage?: boolean, type: 'sign-in' | 'sign-up', automaticRedirect?: boolean, extraInfo?: React.ReactNode, - mockProject?: { - config: { - signUpEnabled: boolean, - credentialEnabled: boolean, - magicLinkEnabled: boolean, - oauthProviders: { - id: string, - }[], - }, - }, + mockProject?: MockProject, }) { const stackApp = useStackApp(); const user = useUser(); @@ -51,7 +54,9 @@ export function AuthPage(props: { return ; } - const enableSeparator = (project.config.credentialEnabled || project.config.magicLinkEnabled) && project.config.oauthProviders.length > 0; + const credentialEnabled = project.config.enabledAuthMethodConfigs.some((x) => x.type === 'password'); + const magicLinkEnabled = project.config.enabledAuthMethodConfigs.some((x) => x.type === 'otp'); + const oauthEnabled = project.config.enabledAuthMethodConfigs.filter((x) => x.type === 'oauth').length > 0; return ( @@ -81,8 +86,8 @@ export function AuthPage(props: { )} - {enableSeparator && } - {project.config.credentialEnabled && project.config.magicLinkEnabled ? ( + {(credentialEnabled || magicLinkEnabled) && oauthEnabled && } + {credentialEnabled && magicLinkEnabled ? ( {t("Magic Link")} @@ -95,9 +100,9 @@ export function AuthPage(props: { {props.type === 'sign-up' ? : } - ) : project.config.credentialEnabled ? ( + ) : credentialEnabled ? ( props.type === 'sign-up' ? : - ) : project.config.magicLinkEnabled ? ( + ) : magicLinkEnabled ? ( ) : null} {props.extraInfo && ( diff --git a/packages/stack/src/components/oauth-button-group.tsx b/packages/stack/src/components/oauth-button-group.tsx index c156dd5df..c7cd04012 100644 --- a/packages/stack/src/components/oauth-button-group.tsx +++ b/packages/stack/src/components/oauth-button-group.tsx @@ -1,5 +1,6 @@ 'use client'; +import { MockProject } from "../components-page/auth-page"; import { useStackApp } from "../lib/hooks"; import { OAuthButton } from "./oauth-button"; @@ -8,20 +9,18 @@ export function OAuthButtonGroup({ mockProject, }: { type: 'sign-in' | 'sign-up', - mockProject?: { - config: { - oauthProviders: { - id: string, - }[], - }, - }, + mockProject?: MockProject, }) { const stackApp = useStackApp(); const project = mockProject || stackApp.useProject(); return (
- {project.config.oauthProviders.map(p => ( - + {project.config.enabledAuthMethodConfigs.map(p => ( + p.type === 'oauth' ? : null ))}
); diff --git a/packages/stack/src/components/oauth-button.tsx b/packages/stack/src/components/oauth-button.tsx index 05d19b108..9f1e1bf5f 100644 --- a/packages/stack/src/components/oauth-button.tsx +++ b/packages/stack/src/components/oauth-button.tsx @@ -5,6 +5,7 @@ import Color from 'color'; import { useId } from 'react'; import { useStackApp } from '..'; import { useTranslation } from '../lib/translations'; +import { throwErr } from '@stackframe/stack-shared/dist/utils/errors'; const iconSize = 22; @@ -156,8 +157,13 @@ export function OAuthButton({ }) { const { t } = useTranslation(); const stackApp = useStackApp(); + const project = stackApp.useProject(); const styleId = useId().replaceAll(':', '-'); + if (provider.length >= 10) { + provider = project.config.enabledOAuthProviderConfigs.find(p => p.id === provider)?.type || throwErr('Invalid provider'); + } + let style : { backgroundColor?: string, textColor?: string, diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 278c8e3e1..2b96bccd7 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -2649,6 +2649,7 @@ export type ProjectConfig = { export type OAuthProviderConfig = { readonly id: string, + readonly type: string, }; export type AuthMethodConfig = { From 7b3bf30b44f4cda53f8edc2522609712c79d306d Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Wed, 18 Sep 2024 17:53:36 -0700 Subject: [PATCH 10/19] project update --- .../src/app/api/v1/projects/current/crud.tsx | 342 +++++------------- .../src/interface/crud/projects.ts | 19 +- 2 files changed, 97 insertions(+), 264 deletions(-) diff --git a/apps/backend/src/app/api/v1/projects/current/crud.tsx b/apps/backend/src/app/api/v1/projects/current/crud.tsx index 9b846f929..53ba916fb 100644 --- a/apps/backend/src/app/api/v1/projects/current/crud.tsx +++ b/apps/backend/src/app/api/v1/projects/current/crud.tsx @@ -1,6 +1,5 @@ import { isTeamSystemPermission, listTeamPermissionDefinitions, teamSystemPermissionStringToDBType } from "@/lib/permissions"; import { fullProjectInclude, projectPrismaToCrud } from "@/lib/projects"; -import { ensureSharedProvider } from "@/lib/request-checks"; import { prismaClient } from "@/prisma-client"; import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { projectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; @@ -8,7 +7,6 @@ import { yupObject } from "@stackframe/stack-shared/dist/schema-fields"; import { StatusError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { createLazyProxy } from "@stackframe/stack-shared/dist/utils/proxies"; import { typedToUppercase } from "@stackframe/stack-shared/dist/utils/strings"; -import { ensureStandardProvider } from "../../../../../lib/request-checks"; export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(projectsCrud, { paramsSchema: yupObject({}), @@ -131,279 +129,117 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro } // ======================= update oauth config ======================= - // loop though all the items from crud.config.oauth_providers - // create the config if it is not already in the DB - // update the config if it is already in the DB - // update/create all auth methods and connected account configs - - const oldProviders = oldProject.config.oauth_providers; - const oauthProviderUpdates = data.config?.oauth_providers; - if (oauthProviderUpdates) { - const providerMap = new Map(oldProviders.map((provider) => [ - provider.id, - { - providerUpdate: (() => { - const update = oauthProviderUpdates.find((p) => p.id === provider.id); - if (!update) { - throw new StatusError(StatusError.BadRequest, `Provider with id '${provider.id}' not found in the update`); - } - return update; - })(), - oldProvider: provider, - } - ])); - - const newProviders = oauthProviderUpdates.map((providerUpdate) => ({ - id: providerUpdate.id, - update: providerUpdate - })).filter(({ id }) => !providerMap.has(id)); - - // Update existing proxied/standard providers - for (const [id, { providerUpdate, oldProvider }] of providerMap) { - // remove existing provider configs - switch (oldProvider.type) { - case 'shared': { - await tx.proxiedOAuthProviderConfig.deleteMany({ - where: { projectConfigId: oldProject.config.id, id: providerUpdate.id }, - }); - break; - } - case 'standard': { - await tx.standardOAuthProviderConfig.deleteMany({ - where: { projectConfigId: oldProject.config.id, id: providerUpdate.id }, - }); - break; - } - } + // 1. check if the old provider config ids is a subset of the new provider config ids + // 2. loop through the new provider config ids + // - if the new provider config id is not in the old provider config ids, create it + // - if the new provider config is in the old provider config ids, remove the proxied/standard oauth config + // - create the new proxied/standard oauth config - // update provider configs with newly created proxied/standard provider configs - let providerConfigUpdate; - if (providerUpdate.type === 'shared') { - providerConfigUpdate = { - proxiedOAuthConfig: { - create: { - type: typedToUppercase(ensureSharedProvider(providerUpdate.id)), - }, - }, - }; - } else { - providerConfigUpdate = { - standardOAuthConfig: { - create: { - type: typedToUppercase(ensureStandardProvider(providerUpdate.id)), - clientId: providerUpdate.client_id ?? throwErr('client_id is required'), - clientSecret: providerUpdate.client_secret ?? throwErr('client_secret is required'), - facebookConfigId: providerUpdate.facebook_config_id, - microsoftTenantId: providerUpdate.microsoft_tenant_id, - }, - }, - }; - } + const oldProviderConfigIds = oldProject.config.enabled_oauth_provider_configs.map(p => p.id); + const newProviderConfigIds = data.config?.oauth_provider_configs?.map(p => p.id) ?? []; - await tx.oAuthProviderConfig.update({ - where: { projectConfigId_id: { projectConfigId: oldProject.config.id, id } }, - data: { - ...providerConfigUpdate, - }, - }); - } - - // Create new providers - for (const provider of newProviders) { - let providerConfigData; - if (provider.update.type === 'shared') { - providerConfigData = { - proxiedOAuthConfig: { - create: { - type: typedToUppercase(ensureSharedProvider(provider.update.id)), - }, - }, - }; - } else { - providerConfigData = { - standardOAuthConfig: { - create: { - type: typedToUppercase(ensureStandardProvider(provider.update.id)), - clientId: provider.update.client_id ?? throwErr('client_id is required'), - clientSecret: provider.update.client_secret ?? throwErr('client_secret is required'), - facebookConfigId: provider.update.facebook_config_id, - microsoftTenantId: provider.update.microsoft_tenant_id, - }, - }, - }; - } - - await tx.oAuthProviderConfig.create({ - data: { - id: provider.id, - projectConfigId: oldProject.config.id, - ...providerConfigData, - }, - }); - } + if (!oldProviderConfigIds.every(id => newProviderConfigIds.includes(id))) { + throw new StatusError(StatusError.BadRequest, `Invalid OAuth provider configuration IDs. Removal of provider configurations is not allowed.`); + } - // Update/create auth methods and connected account configs - const providers = await tx.oAuthProviderConfig.findMany({ + for (const newConfig of data.config?.oauth_provider_configs ?? []) { + const createdConfig = await tx.oAuthProviderConfig.upsert({ where: { + projectConfigId_id: { + projectConfigId: oldProject.config.id, + id: newConfig.id, + } + }, + create: { + id: newConfig.id, projectConfigId: oldProject.config.id, }, - include: { - standardOAuthConfig: true, - proxiedOAuthConfig: true, - } + update: { + proxiedOAuthConfig: { + delete: true, + }, + standardOAuthConfig: { + delete: true, + }, + }, }); - for (const provider of providers) { - const enabled = oauthProviderUpdates.find((p) => p.id === provider.id)?.enabled ?? false; - - const authMethod = await tx.authMethodConfig.findFirst({ - where: { - projectConfigId: oldProject.config.id, - oauthProviderConfig: { - id: provider.id, - }, - } - }); - - if (!authMethod) { - await tx.authMethodConfig.create({ - data: { - projectConfigId: oldProject.config.id, - enabled, - oauthProviderConfig: { - connect: { - projectConfigId_id: { - projectConfigId: oldProject.config.id, - id: provider.id, - } - } - } - }, - }); - } else { - await tx.authMethodConfig.update({ - where: { - projectConfigId_id: { - projectConfigId: oldProject.config.id, - id: authMethod.id, - } - }, - data: { - enabled, - }, - }); - } - - const connectedAccount = await tx.connectedAccountConfig.findFirst({ - where: { - projectConfigId: oldProject.config.id, - oauthProviderConfig: { - id: provider.id, - }, - } - }); - - if (!connectedAccount) { - if (provider.standardOAuthConfig) { - await tx.connectedAccountConfig.create({ - data: { - projectConfigId: oldProject.config.id, - enabled, - oauthProviderConfig: { - connect: { - projectConfigId_id: { - projectConfigId: oldProject.config.id, - id: provider.id, - } - } - } - }, - }); - } - } else { - await tx.connectedAccountConfig.update({ - where: { - projectConfigId_id: { - projectConfigId: oldProject.config.id, - id: connectedAccount.id, - } - }, - data: { - enabled: provider.standardOAuthConfig ? enabled : false, - }, - }); - } - } - } - // ======================= update password auth method ======================= - const passwordAuth = await tx.passwordAuthMethodConfig.findFirst({ - where: { - projectConfigId: oldProject.config.id, - identifierType: "EMAIL", - }, - }); - if (data.config?.credential_enabled !== undefined) { - if (!passwordAuth) { - await tx.authMethodConfig.create({ + if (newConfig.shared) { + await tx.proxiedOAuthProviderConfig.create({ data: { projectConfigId: oldProject.config.id, - enabled: data.config.credential_enabled, - passwordConfig: { - create: { - identifierType: "EMAIL", - }, - }, + id: createdConfig.id, + type: typedToUppercase(newConfig.type), }, }); } else { - await tx.authMethodConfig.update({ - where: { - projectConfigId_id: { - projectConfigId: oldProject.config.id, - id: passwordAuth.authMethodConfigId, - }, - }, + await tx.standardOAuthProviderConfig.create({ data: { - enabled: data.config.credential_enabled, + projectConfigId: oldProject.config.id, + id: createdConfig.id, + type: typedToUppercase(newConfig.type), + clientId: newConfig.client_id, + clientSecret: newConfig.client_secret, + facebookConfigId: newConfig.facebook_config_id, + microsoftTenantId: newConfig.microsoft_tenant_id, }, }); } } - // ======================= update OTP auth method ======================= - const otpAuth = await tx.otpAuthMethodConfig.findFirst({ - where: { - projectConfigId: oldProject.config.id, - }, - }); - if (data.config?.magic_link_enabled !== undefined) { - if (!otpAuth) { - await tx.authMethodConfig.create({ - data: { - projectConfigId: oldProject.config.id, - enabled: data.config.magic_link_enabled, - otpConfig: { - create: { - contactChannelType: "EMAIL", - }, - }, - }, - }); - } else { - await tx.authMethodConfig.update({ - where: { - projectConfigId_id: { - projectConfigId: oldProject.config.id, - id: otpAuth.authMethodConfigId, - }, - }, - data: { - enabled: data.config.magic_link_enabled, - }, - }); + // ======================= auth methods ======================= + // 1. check if the old auth method ids is a subset of the new auth method ids + // 2. check if the auth method types are still the same + // 3. loop through all the auth methods + // - create/update the auth method + + for (const oldAuthMethod of oldProject.config.auth_method_configs) { + const newAuthMethod = data.config?.auth_method_configs?.find(p => p.id === oldAuthMethod.id); + if (!newAuthMethod) { + throw new StatusError(StatusError.BadRequest, `Auth method config ID ${oldAuthMethod.id} not found`); + } + + if (newAuthMethod.type !== oldAuthMethod.type) { + throw new StatusError(StatusError.BadRequest, `Auth method type mismatch for ID ${oldAuthMethod.id}`); } } + for (const newAuthMethod of data.config?.auth_method_configs ?? []) { + await tx.authMethodConfig.upsert({ + where: { + projectConfigId_id: { + projectConfigId: oldProject.config.id, + id: newAuthMethod.id, + } + }, + create: { + id: newAuthMethod.id, + projectConfigId: oldProject.config.id, + ...(() => { + switch (newAuthMethod.type) { + case 'password': { + return { + type: 'password', + }; + } + case 'otp': { + return { + type: 'otp', + }; + } + case 'oauth': { + return { + type: 'oauth', + providerConfigId: newAuthMethod.provider_config_id, + }; + } + } + })() + }, + update: {}, + }); + } + // ======================= update the rest ======================= // check domain uniqueness diff --git a/packages/stack-shared/src/interface/crud/projects.ts b/packages/stack-shared/src/interface/crud/projects.ts index a9a25189e..6da0131f5 100644 --- a/packages/stack-shared/src/interface/crud/projects.ts +++ b/packages/stack-shared/src/interface/crud/projects.ts @@ -1,23 +1,20 @@ import { CrudTypeOf, createCrud } from "../../crud"; import * as schemaFields from "../../schema-fields"; import { yupArray, yupObject, yupRequiredWhen, yupString } from "../../schema-fields"; -import { allProviders } from "../../utils/oauth"; +import { allProviders, sharedProviders, standardProviders } from "../../utils/oauth"; const teamPermissionSchema = yupObject({ id: yupString().required(), }).required(); - -const oauthProviderConfigSharedFields = { - id: yupString().required(), - type: yupString().oneOf(allProviders).required(), -}; const oauthProviderConfigSchema = schemaFields.yupUnion( yupObject({ - ...oauthProviderConfigSharedFields, + id: yupString().uuid().required(), + type: yupString().oneOf(sharedProviders).required(), shared: schemaFields.yupBoolean().isTrue().required(), }).required(), yupObject({ - ...oauthProviderConfigSharedFields, + id: yupString().uuid().required(), + type: yupString().oneOf(standardProviders).required(), shared: schemaFields.yupBoolean().isFalse().required(), client_id: yupString().required(), client_secret: yupString().required(), @@ -27,12 +24,12 @@ const oauthProviderConfigSchema = schemaFields.yupUnion( ); const clientOAuthProviderConfigSchema = yupObject({ - id: yupString().required(), + id: yupString().uuid().required(), type: yupString().oneOf(allProviders).required(), }).required(); const authMethodSharedFields = { - id: yupString().required(), + id: yupString().uuid().required(), enabled: schemaFields.yupBoolean().required(), }; const authMethodConfigSchema = schemaFields.yupUnion( @@ -54,7 +51,7 @@ const authMethodConfigSchema = schemaFields.yupUnion( const clientAuthMethodConfigSchema = authMethodConfigSchema; const connectedAccountConfigSchema = yupObject({ - id: yupString().required(), + id: yupString().uuid().required(), enabled: schemaFields.yupBoolean().required(), provider_id: yupString().required(), }); From 410a8672b6782104cb8058639f90228358c0c173 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Wed, 25 Sep 2024 15:24:34 -0700 Subject: [PATCH 11/19] improved error message on yupUnion --- apps/backend/src/app/api/v1/internal/projects/crud.tsx | 4 ++-- packages/stack-shared/src/schema-fields.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/backend/src/app/api/v1/internal/projects/crud.tsx b/apps/backend/src/app/api/v1/internal/projects/crud.tsx index 86561a0de..8175b2eee 100644 --- a/apps/backend/src/app/api/v1/internal/projects/crud.tsx +++ b/apps/backend/src/app/api/v1/internal/projects/crud.tsx @@ -76,8 +76,8 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand standardOAuthConfig: !item.shared ? { create: { type: typedToUppercase(ensureStandardProvider(item.type)), - clientId: item.client_id ?? throwErr('client_id is required'), - clientSecret: item.client_secret ?? throwErr('client_secret is required'), + clientId: item.client_id, + clientSecret: item.client_secret, facebookConfigId: item.facebook_config_id, microsoftTenantId: item.microsoft_tenant_id, } diff --git a/packages/stack-shared/src/schema-fields.ts b/packages/stack-shared/src/schema-fields.ts index 117218760..972c5a7ae 100644 --- a/packages/stack-shared/src/schema-fields.ts +++ b/packages/stack-shared/src/schema-fields.ts @@ -146,7 +146,10 @@ export function yupUnion[]>(...args: T): yup.ISchema< errors.push(e); } } - throw new AggregateError(errors, 'Invalid value; must be one of the provided schemas'); + return context.createError({ + message: `${context.path} is not matched by any of the provided schemas. \nInner errors on each schema: \n${errors.map((e: any, i) => '\tSchema ' + i + ": \n\t\t" + e.errors.join('\n\t\t')).join('\n')}`, + path: context.path, + }); }); } From a435d3bf6e8b347fb087f642b142455d8c79e5c7 Mon Sep 17 00:00:00 2001 From: Zai Shi Date: Wed, 25 Sep 2024 16:13:19 -0700 Subject: [PATCH 12/19] fixed internal project tests --- .../api/v1/internal/projects.test.ts | 162 +++++++++++++----- 1 file changed, 118 insertions(+), 44 deletions(-) diff --git a/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts b/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts index 9957ee3d4..76a9f3b6d 100644 --- a/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts +++ b/apps/e2e/tests/backend/endpoints/api/v1/internal/projects.test.ts @@ -66,16 +66,21 @@ it("creates a new project", async ({ expect }) => { "body": { "config": { "allow_localhost": true, + "auth_method_configs": [], "client_team_creation_enabled": false, "client_user_deletion_enabled": false, + "connected_account_configs": [], "create_team_on_sign_up": false, - "credential_enabled": true, + "credential_enabled": false, "domains": [], "email_config": { "type": "shared" }, + "enabled_auth_method_configs": [], + "enabled_connected_account_configs": [], + "enabled_oauth_provider_configs": [], "enabled_oauth_providers": [], "id": "", "magic_link_enabled": false, - "oauth_providers": [], + "oauth_provider_configs": [], "sign_up_enabled": true, "team_creator_default_permissions": [{ "id": "admin" }], "team_member_default_permissions": [{ "id": "member" }], @@ -92,36 +97,68 @@ it("creates a new project", async ({ expect }) => { `); }); -it("creates a new project with different configurations", async ({ expect }) => { +it("creates a new project with auth method configs", async ({ expect }) => { backendContext.set({ projectKeys: InternalProjectClientKeys }); await Auth.Otp.signIn(); - const { createProjectResponse: response1 } = await Project.create({ + const { createProjectResponse: response } = await Project.create({ display_name: "Test Project", description: "Test description", is_production_mode: true, config: { allow_localhost: false, sign_up_enabled: false, - credential_enabled: false, - magic_link_enabled: true, + auth_method_configs: [ + { + id: crypto.randomUUID(), + type: 'otp', + enabled: true, + }, + { + id: crypto.randomUUID(), + type: 'password', + enabled: false, + } + ] }, }); - expect(response1).toMatchInlineSnapshot(` + expect(response).toMatchInlineSnapshot(` NiceResponse { "status": 201, "body": { "config": { "allow_localhost": false, + "auth_method_configs": [ + { + "enabled": true, + "id": "", + "type": "otp", + }, + { + "enabled": false, + "id": "", + "type": "password", + }, + ], "client_team_creation_enabled": false, "client_user_deletion_enabled": false, + "connected_account_configs": [], "create_team_on_sign_up": false, "credential_enabled": false, "domains": [], "email_config": { "type": "shared" }, + "enabled_auth_method_configs": [ + { + "enabled": true, + "id": "", + "type": "otp", + }, + ], + "enabled_connected_account_configs": [], + "enabled_oauth_provider_configs": [], "enabled_oauth_providers": [], "id": "", "magic_link_enabled": true, - "oauth_providers": [], + "oauth_provider_configs": [], "sign_up_enabled": false, "team_creator_default_permissions": [{ "id": "admin" }], "team_member_default_permissions": [{ "id": "member" }], @@ -136,54 +173,62 @@ it("creates a new project with different configurations", async ({ expect }) => "headers": Headers {