diff --git a/apps/backend/prisma/migrations/20240726164717_removed_shared_spotify/migration.sql b/apps/backend/prisma/migrations/20240726164717_removed_shared_spotify/migration.sql new file mode 100644 index 0000000000..fe073965d1 --- /dev/null +++ b/apps/backend/prisma/migrations/20240726164717_removed_shared_spotify/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - The values [SPOTIFY] on the enum `ProxiedOAuthProviderType` will be removed. If these variants are still used in the database, this will fail. + +*/ +-- AlterEnum +BEGIN; +CREATE TYPE "ProxiedOAuthProviderType_new" AS ENUM ('GITHUB', 'FACEBOOK', 'GOOGLE', 'MICROSOFT'); +ALTER TABLE "ProxiedOAuthProviderConfig" ALTER COLUMN "type" TYPE "ProxiedOAuthProviderType_new" USING ("type"::text::"ProxiedOAuthProviderType_new"); +ALTER TYPE "ProxiedOAuthProviderType" RENAME TO "ProxiedOAuthProviderType_old"; +ALTER TYPE "ProxiedOAuthProviderType_new" RENAME TO "ProxiedOAuthProviderType"; +DROP TYPE "ProxiedOAuthProviderType_old"; +COMMIT; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index 9abafeca1d..f9eb095042 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -517,7 +517,6 @@ enum ProxiedOAuthProviderType { FACEBOOK GOOGLE MICROSOFT - SPOTIFY } model StandardOAuthProviderConfig { diff --git a/apps/backend/src/app/api/v1/auth/oauth/authorize/[provider]/route.tsx b/apps/backend/src/app/api/v1/auth/oauth/authorize/[provider]/route.tsx index 4e064cfb83..32da0fed50 100644 --- a/apps/backend/src/app/api/v1/auth/oauth/authorize/[provider]/route.tsx +++ b/apps/backend/src/app/api/v1/auth/oauth/authorize/[provider]/route.tsx @@ -4,7 +4,6 @@ import { decodeAccessToken, oauthCookieSchema } from "@/lib/tokens"; import { getProvider } from "@/oauth"; import { prismaClient } from "@/prisma-client"; import { createSmartRouteHandler } from "@/route-handlers/smart-route-handler"; -import { sharedProviders } from "@stackframe/stack-shared/dist/interface/clientInterface"; import { KnownErrors } from "@stackframe/stack-shared/dist/known-errors"; import { yupNumber, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; import { getNodeEnvironment } from "@stackframe/stack-shared/dist/utils/env"; @@ -77,7 +76,7 @@ export const GET = createSmartRouteHandler({ throw new StatusError(StatusError.Forbidden, "The access token is not valid for this project"); } - if (query.provider_scope && sharedProviders.includes(provider.type as any)) { + if (query.provider_scope && provider.type === "shared") { throw new KnownErrors.OAuthExtraScopeNotAvailableWithSharedOAuthKeys(); } projectUserId = userId; diff --git a/apps/backend/src/app/api/v1/auth/oauth/connected-accounts/[provider]/access-token/crud.tsx b/apps/backend/src/app/api/v1/auth/oauth/connected-accounts/[provider]/access-token/crud.tsx index 54e52dd8e1..28251d7130 100644 --- a/apps/backend/src/app/api/v1/auth/oauth/connected-accounts/[provider]/access-token/crud.tsx +++ b/apps/backend/src/app/api/v1/auth/oauth/connected-accounts/[provider]/access-token/crud.tsx @@ -4,7 +4,7 @@ import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { providerAccessTokenCrud } from "@stackframe/stack-shared/dist/interface/crud/oauth"; import { yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; -import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; +import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; import { extractScopes } from "@stackframe/stack-shared/dist/utils/strings"; 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 0ed37286f3..83280cf079 100644 --- a/apps/backend/src/app/api/v1/internal/projects/crud.tsx +++ b/apps/backend/src/app/api/v1/internal/projects/crud.tsx @@ -1,9 +1,10 @@ import { fullProjectInclude, listManagedProjectIds, projectPrismaToCrud } from "@/lib/projects"; +import { ensureSharedProvider, ensureStandardProvider } from "@/lib/request-checks"; import { prismaClient } from "@/prisma-client"; import { createCrudHandlers } from "@/route-handlers/crud-handler"; import { KnownErrors } from "@stackframe/stack-shared"; import { internalProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; -import { projectIdSchema, yupObject, yupString } from "@stackframe/stack-shared/dist/schema-fields"; +import { projectIdSchema, yupObject } from "@stackframe/stack-shared/dist/schema-fields"; import { StackAssertionError, 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"; @@ -49,12 +50,12 @@ export const internalProjectsCrudHandlers = createLazyProxy(() => createCrudHand enabled: item.enabled, proxiedOAuthConfig: item.type === "shared" ? { create: { - type: typedToUppercase(item.id), + type: typedToUppercase(ensureSharedProvider(item.id)), } } : undefined, standardOAuthConfig: item.type === "standard" ? { create: { - type: typedToUppercase(item.id), + type: typedToUppercase(ensureStandardProvider(item.id)), clientId: item.client_id ?? throwErr('client_id is required'), clientSecret: item.client_secret ?? throwErr('client_secret is required'), } 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 ce15d12c5d..d5095278fa 100644 --- a/apps/backend/src/app/api/v1/projects/current/crud.tsx +++ b/apps/backend/src/app/api/v1/projects/current/crud.tsx @@ -1,5 +1,6 @@ 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"; @@ -7,6 +8,7 @@ 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({}), @@ -180,7 +182,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro providerConfigUpdate = { proxiedOAuthConfig: { create: { - type: typedToUppercase(providerUpdate.id), + type: typedToUppercase(ensureSharedProvider(providerUpdate.id)), }, }, }; @@ -188,7 +190,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro providerConfigUpdate = { standardOAuthConfig: { create: { - type: typedToUppercase(providerUpdate.id), + type: typedToUppercase(ensureStandardProvider(providerUpdate.id)), clientId: providerUpdate.client_id ?? throwErr('client_id is required'), clientSecret: providerUpdate.client_secret ?? throwErr('client_secret is required'), }, @@ -212,7 +214,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro providerConfigData = { proxiedOAuthConfig: { create: { - type: typedToUppercase(provider.update.id), + type: typedToUppercase(ensureSharedProvider(provider.update.id)), }, }, }; @@ -220,7 +222,7 @@ export const projectsCrudHandlers = createLazyProxy(() => createCrudHandlers(pro providerConfigData = { standardOAuthConfig: { create: { - type: typedToUppercase(provider.update.id), + 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'), }, diff --git a/apps/backend/src/lib/projects.tsx b/apps/backend/src/lib/projects.tsx index b24f114075..2153f25dfa 100644 --- a/apps/backend/src/lib/projects.tsx +++ b/apps/backend/src/lib/projects.tsx @@ -1,11 +1,12 @@ import { usersCrudHandlers } from "@/app/api/v1/users/crud"; import { prismaClient } from "@/prisma-client"; import { CrudHandlerInvocationError } from "@/route-handlers/crud-handler"; -import { Prisma, ProxiedOAuthProviderType } 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, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { ProviderType } from "@stackframe/stack-shared/dist/utils/oauth"; import { typedToLowercase } from "@stackframe/stack-shared/dist/utils/strings"; import { fullPermissionInclude, teamPermissionDefinitionJsonFromDbType, teamPermissionDefinitionJsonFromTeamSystemDbType } from "./permissions"; import { decodeAccessToken } from "./tokens"; @@ -61,7 +62,7 @@ export function projectPrismaToCrud( ): ProjectsCrud["Admin"]["Read"] { const oauthProviders = prisma.config.oauthProviderConfigs .flatMap((provider): { - id: Lowercase, + id: ProviderType, enabled: boolean, type: 'standard' | 'shared', client_id?: string | undefined, diff --git a/apps/backend/src/lib/request-checks.tsx b/apps/backend/src/lib/request-checks.tsx index 6058c28aac..06ba1b6184 100644 --- a/apps/backend/src/lib/request-checks.tsx +++ b/apps/backend/src/lib/request-checks.tsx @@ -1,7 +1,9 @@ +import { ProxiedOAuthProviderType, StandardOAuthProviderType } from "@prisma/client"; import { KnownErrors } from "@stackframe/stack-shared"; -import { PrismaTransaction } from "./types"; -import { TeamSystemPermission, listUserTeamPermissions } from "./permissions"; import { ProjectsCrud } from "@stackframe/stack-shared/dist/interface/crud/projects"; +import { ProviderType, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/utils/oauth"; +import { TeamSystemPermission, listUserTeamPermissions } from "./permissions"; +import { PrismaTransaction } from "./types"; async function _getTeamMembership( @@ -122,4 +124,22 @@ export async function ensureUserExist( if (!user) { throw new KnownErrors.UserNotFound(); } +} + +export function ensureSharedProvider( + providerId: ProviderType +): Lowercase { + if (!sharedProviders.includes(providerId as any)) { + throw new KnownErrors.InvalidSharedOAuthProviderId(providerId); + } + return providerId as any; +} + +export function ensureStandardProvider( + providerId: ProviderType +): Lowercase { + if (!standardProviders.includes(providerId as any)) { + throw new KnownErrors.InvalidStandardOAuthProviderId(providerId); + } + return providerId as any; } \ No newline at end of file diff --git a/apps/dashboard/prisma/schema.prisma b/apps/dashboard/prisma/schema.prisma index 9abafeca1d..f9eb095042 100644 --- a/apps/dashboard/prisma/schema.prisma +++ b/apps/dashboard/prisma/schema.prisma @@ -517,7 +517,6 @@ enum ProxiedOAuthProviderType { FACEBOOK GOOGLE MICROSOFT - SPOTIFY } model StandardOAuthProviderConfig { diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx index 662ae32e80..443456626c 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/auth-methods/page-client.tsx @@ -1,8 +1,9 @@ "use client"; -import { useAdminApp } from "../use-admin-app"; -import { ProviderSettingSwitch, availableProviders } from "./providers"; -import { PageLayout } from "../page-layout"; import { SettingCard, SettingSwitch } from "@/components/settings"; +import { allProviders } from "@stackframe/stack-shared/dist/utils/oauth"; +import { PageLayout } from "../page-layout"; +import { useAdminApp } from "../use-admin-app"; +import { ProviderSettingSwitch } from "./providers"; export default function PageClient() { const stackAdminApp = useAdminApp(); @@ -37,7 +38,7 @@ export default function PageClient() { - {availableProviders.map((id) => { + {allProviders.map((id) => { const provider = oauthProviders.find((provider) => provider.id === id); return Promise, }; -export const availableProviders = ['github', 'google', 'facebook', 'microsoft', 'spotify'] as const; function toTitle(id: string) { return { github: "GitHub", @@ -48,9 +48,10 @@ export const providerFormSchema = yup.object({ export type ProviderFormValues = yup.InferType -export function ProviderSettingDialog(props: Props) { +export function ProviderSettingDialog(props: Props & { open: boolean, onClose: () => void }) { + const hasSharedKeys = sharedProviders.includes(props.id as any); const defaultValues = { - shared: props.provider?.type === 'shared', + shared: props.provider ? (props.provider.type === 'shared') : hasSharedKeys, clientId: (props.provider as any)?.clientId ?? "", clientSecret: (props.provider as any)?.clientSecret ?? "", }; @@ -74,17 +75,22 @@ export function ProviderSettingDialog(props: Props) { defaultValues={defaultValues} formSchema={providerFormSchema} onSubmit={onSubmit} - trigger={} + open={props.open} + onClose={props.onClose} title={`${toTitle(props.id)} OAuth provider`} cancelButton okButton={{ label: 'Save' }} render={(form) => ( <> - + {hasSharedKeys ? + : + + This OAuth provider does not support shared keys + } {form.watch("shared") ? @@ -155,6 +161,7 @@ export function ProviderSettingSwitch(props: Props) { const enabled = !!props.provider?.enabled; const isShared = props.provider?.type === 'shared'; const [TurnOffProviderDialogOpen, setTurnOffProviderDialogOpen] = useState(false); + const [ProviderSettingDialogOpen, setProviderSettingDialogOpen] = useState(false); const updateProvider = async (checked: boolean) => { await props.updateProvider({ @@ -184,10 +191,10 @@ export function ProviderSettingSwitch(props: Props) { setTurnOffProviderDialogOpen(true); return; } else { - await updateProvider(checked); + setProviderSettingDialogOpen(true); } }} - actions={} + actions={ setProviderSettingDialogOpen(true)} />} onlyShowActionsWhenChecked /> @@ -197,6 +204,8 @@ export function ProviderSettingSwitch(props: Props) { providerId={props.id} onConfirm={() => runAsynchronously(updateProvider(false))} /> + + setProviderSettingDialogOpen(false)} /> ); } diff --git a/apps/dashboard/src/app/api/v1/auth/access-token/[provider]/route.tsx b/apps/dashboard/src/app/api/v1/auth/access-token/[provider]/route.tsx index ed6d667f6d..19e9d55169 100644 --- a/apps/dashboard/src/app/api/v1/auth/access-token/[provider]/route.tsx +++ b/apps/dashboard/src/app/api/v1/auth/access-token/[provider]/route.tsx @@ -1,8 +1,8 @@ import { getProvider } from "@/oauth"; import { prismaClient } from "@/prisma-client"; import { createCrudHandlers } from "@/route-handlers/crud-handler"; +import { sharedProviders } from "@/temporary-types"; import { KnownErrors } from "@stackframe/stack-shared"; -import { sharedProviders } from "@stackframe/stack-shared/dist/interface/clientInterface"; import { accessTokenCrud } from "@stackframe/stack-shared/dist/interface/crud-deprecated/oauth"; import { StackAssertionError, StatusError } from "@stackframe/stack-shared/dist/utils/errors"; import { extractScopes } from "@stackframe/stack-shared/dist/utils/strings"; diff --git a/apps/dashboard/src/app/api/v1/auth/authorize/[provider]/route.tsx b/apps/dashboard/src/app/api/v1/auth/authorize/[provider]/route.tsx index 91cc5dda3d..0630d6e21e 100644 --- a/apps/dashboard/src/app/api/v1/auth/authorize/[provider]/route.tsx +++ b/apps/dashboard/src/app/api/v1/auth/authorize/[provider]/route.tsx @@ -10,8 +10,8 @@ import { getProject } from "@/lib/projects"; import { checkApiKeySet } from "@/lib/api-keys"; import { KnownErrors } from "@stackframe/stack-shared"; import { decodeAccessToken, oauthCookieSchema } from "@/lib/tokens"; -import { sharedProviders } from "@stackframe/stack-shared/dist/interface/clientInterface"; import { prismaClient } from "@/prisma-client"; +import { sharedProviders } from "@/temporary-types"; const expireMinutes = 10; diff --git a/apps/dashboard/src/components/data-table/user-table.tsx b/apps/dashboard/src/components/data-table/user-table.tsx index e6ac78a17c..9f27253bab 100644 --- a/apps/dashboard/src/components/data-table/user-table.tsx +++ b/apps/dashboard/src/components/data-table/user-table.tsx @@ -1,11 +1,14 @@ 'use client'; +import { useAdminApp } from '@/app/(main)/(protected)/projects/[projectId]/use-admin-app'; import { ServerUser } from '@stackframe/stack'; -import { standardProviders } from "@stackframe/stack-shared/dist/interface/clientInterface"; -import { jsonStringOrEmptySchema, jsonStringSchema } from "@stackframe/stack-shared/dist/schema-fields"; +import { jsonStringOrEmptySchema } from "@stackframe/stack-shared/dist/schema-fields"; +import { allProviders } from '@stackframe/stack-shared/dist/utils/oauth'; +import { deindent } from '@stackframe/stack-shared/dist/utils/strings'; import { ColumnDef, Row, Table } from "@tanstack/react-table"; import { useMemo, useState } from "react"; import * as yup from "yup"; import { ActionDialog } from "../action-dialog"; +import { CopyField } from '../copy-field'; import { FormDialog } from "../form-dialog"; import { DateField, InputField, SwitchField, TextAreaField } from "../form-fields"; import { SimpleTooltip } from "../simple-tooltip"; @@ -16,10 +19,6 @@ import { DataTable } from "./elements/data-table"; import { DataTableFacetedFilter } from "./elements/faceted-filter"; import { SearchToolbarItem } from "./elements/toolbar-items"; import { arrayFilterFn, standardFilterFn } from "./elements/utils"; -import { wait } from '@stackframe/stack-shared/dist/utils/promises'; -import { CopyField } from '../copy-field'; -import { deindent } from '@stackframe/stack-shared/dist/utils/strings'; -import { useAdminApp } from '@/app/(main)/(protected)/projects/[projectId]/use-admin-app'; export type ExtendedServerUser = ServerUser & { authTypes: string[], @@ -33,7 +32,7 @@ function userToolbarRender(table: Table) { ({ + options={['email', 'password', ...allProviders].map((provider) => ({ value: provider, label: provider, }))} diff --git a/apps/dashboard/src/lib/projects.tsx b/apps/dashboard/src/lib/projects.tsx index d616b90f26..a6d79e9362 100644 --- a/apps/dashboard/src/lib/projects.tsx +++ b/apps/dashboard/src/lib/projects.tsx @@ -1,15 +1,13 @@ -import * as yup from "yup"; -import { OAuthProviderConfigJson, ProjectJson, ServerUserJson, EmailConfigJson } from "@/temporary-types"; -import { Prisma, ProxiedOAuthProviderType, StandardOAuthProviderType } from "@prisma/client"; import { prismaClient } from "@/prisma-client"; +import { EmailConfigJson, OAuthProviderConfigJson, OAuthProviderUpdateOptions, ProjectJson, ProjectUpdateOptions, ServerUserJson, SharedProvider, StandardProvider, sharedProviders, standardProviders } from "@/temporary-types"; +import { Prisma, ProxiedOAuthProviderType, StandardOAuthProviderType } from "@prisma/client"; +import { KnownErrors } from "@stackframe/stack-shared"; +import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; +import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; +import * as yup from "yup"; +import { fullPermissionInclude, isTeamSystemPermission, listServerPermissionDefinitions, serverPermissionDefinitionJsonFromDbType, serverPermissionDefinitionJsonFromTeamSystemDbType, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions"; import { decodeAccessToken } from "./tokens"; import { getServerUser } from "./users"; -import { generateUuid } from "@stackframe/stack-shared/dist/utils/uuids"; -import { SharedProvider, StandardProvider, sharedProviders, standardProviders } from "@stackframe/stack-shared/dist/interface/clientInterface"; -import { OAuthProviderUpdateOptions, ProjectUpdateOptions } from "@/temporary-types"; -import { StackAssertionError, StatusError, captureError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; -import { fullPermissionInclude, isTeamSystemPermission, listServerPermissionDefinitions, serverPermissionDefinitionJsonFromDbType, serverPermissionDefinitionJsonFromTeamSystemDbType, teamDBTypeToSystemPermissionString, teamPermissionIdSchema, teamSystemPermissionStringToDBType } from "./permissions"; -import { KnownErrors } from "@stackframe/stack-shared"; function toDBSharedProvider(type: SharedProvider): ProxiedOAuthProviderType { return ({ @@ -18,7 +16,7 @@ function toDBSharedProvider(type: SharedProvider): ProxiedOAuthProviderType { "shared-facebook": "FACEBOOK", "shared-microsoft": "MICROSOFT", "shared-spotify": "SPOTIFY", - } as const)[type]; + } as any)[type]; } function toDBStandardProvider(type: StandardProvider): StandardOAuthProviderType { diff --git a/apps/dashboard/src/oauth/index.tsx b/apps/dashboard/src/oauth/index.tsx index c7f14db851..d96b9d4852 100644 --- a/apps/dashboard/src/oauth/index.tsx +++ b/apps/dashboard/src/oauth/index.tsx @@ -1,5 +1,5 @@ import OAuth2Server from "@node-oauth/oauth2-server"; -import { OAuthProviderConfigJson } from "@/temporary-types"; +import { OAuthProviderConfigJson, SharedProvider, sharedProviders, toStandardProvider } from "@/temporary-types"; import { getEnvVariable } from "@stackframe/stack-shared/dist/utils/env"; import { GithubProvider } from "./providers/github"; import { OAuthModel } from "./model"; @@ -8,7 +8,6 @@ import { GoogleProvider } from "./providers/google"; import { FacebookProvider } from "./providers/facebook"; import { MicrosoftProvider } from "./providers/microsoft"; import { SpotifyProvider } from "./providers/spotify"; -import { SharedProvider, sharedProviders, toStandardProvider } from "@stackframe/stack-shared/dist/interface/clientInterface"; const _providers = { github: GithubProvider, diff --git a/apps/dashboard/src/temporary-types.ts b/apps/dashboard/src/temporary-types.ts index 89c4dd7f0c..b1b3ac8dc0 100644 --- a/apps/dashboard/src/temporary-types.ts +++ b/apps/dashboard/src/temporary-types.ts @@ -1,6 +1,5 @@ // TODO next-release: Remove this file when we remove the dashboard API. -import { SharedProvider, StandardProvider } from "@stackframe/stack-shared/dist/interface/clientInterface"; import { ReadonlyJson } from "@stackframe/stack-shared/dist/utils/json"; export type ServerTeamMemberJson = TeamMemberJson & { @@ -218,3 +217,29 @@ export type ServerUserUpdateJson = UserUpdateJson & { primaryEmail?: string | null, primaryEmailVerified?: boolean, } + +export type SharedProvider = "shared-github" | "shared-google" | "shared-facebook" | "shared-microsoft" | "shared-spotify"; +export const sharedProviders = [ + "shared-github", + "shared-google", + "shared-facebook", + "shared-microsoft", + "shared-spotify", +] as const; + +export type StandardProvider = "github" | "facebook" | "google" | "microsoft" | "spotify"; +export const standardProviders = [ + "github", + "facebook", + "google", + "microsoft", + "spotify", +] as const; + +export function toStandardProvider(provider: SharedProvider | StandardProvider): StandardProvider { + return provider.replace("shared-", "") as StandardProvider; +} + +export function toSharedProvider(provider: SharedProvider | StandardProvider): SharedProvider { + return "shared-" + provider as SharedProvider; +} diff --git a/packages/stack-shared/src/interface/clientInterface.ts b/packages/stack-shared/src/interface/clientInterface.ts index f02f9f5247..3a3aa16474 100644 --- a/packages/stack-shared/src/interface/clientInterface.ts +++ b/packages/stack-shared/src/interface/clientInterface.ts @@ -24,33 +24,6 @@ export type ClientInterfaceOptions = { projectOwnerSession: InternalSession, }); -export type SharedProvider = "shared-github" | "shared-google" | "shared-facebook" | "shared-microsoft" | "shared-spotify"; -export const sharedProviders = [ - "shared-github", - "shared-google", - "shared-facebook", - "shared-microsoft", - "shared-spotify", -] as const; - -export type StandardProvider = "github" | "facebook" | "google" | "microsoft" | "spotify"; -export const standardProviders = [ - "github", - "facebook", - "google", - "microsoft", - "spotify", -] as const; - -export function toStandardProvider(provider: SharedProvider | StandardProvider): StandardProvider { - return provider.replace("shared-", "") as StandardProvider; -} - -export function toSharedProvider(provider: SharedProvider | StandardProvider): SharedProvider { - return "shared-" + provider as SharedProvider; -} - - export class StackClientInterface { constructor(public readonly options: ClientInterfaceOptions) { // nothing here diff --git a/packages/stack-shared/src/interface/crud/oauth.ts b/packages/stack-shared/src/interface/crud/oauth.ts index c1bd3f5927..2a326f7a19 100644 --- a/packages/stack-shared/src/interface/crud/oauth.ts +++ b/packages/stack-shared/src/interface/crud/oauth.ts @@ -13,4 +13,4 @@ export const providerAccessTokenCrud = createCrud({ clientReadSchema: providerAccessTokenReadSchema, clientCreateSchema: providerAccessTokenCreateSchema, }); -export type ProviderAccessTokenCrud = CrudTypeOf; +export type ProviderAccessTokenCrud = CrudTypeOf; \ No newline at end of file diff --git a/packages/stack-shared/src/known-errors.tsx b/packages/stack-shared/src/known-errors.tsx index c3234c3db3..784fb1e7f3 100644 --- a/packages/stack-shared/src/known-errors.tsx +++ b/packages/stack-shared/src/known-errors.tsx @@ -1034,6 +1034,32 @@ const TeamPermissionRequired = createKnownErrorConstructor( (json) => [json.team_id, json.user_id, json.permission_id] as const, ); +const InvalidSharedOAuthProviderId = createKnownErrorConstructor( + KnownError, + "INVALID_SHARED_OAUTH_PROVIDER_ID", + (providerId) => [ + 400, + `The shared OAuth provider with ID ${providerId} is not valid.`, + { + provider_id: providerId, + }, + ] as const, + (json) => [json.provider_id] as const, +); + +const InvalidStandardOAuthProviderId = createKnownErrorConstructor( + KnownError, + "INVALID_STANDARD_OAUTH_PROVIDER_ID", + (providerId) => [ + 400, + `The standard OAuth provider with ID ${providerId} is not valid.`, + { + provider_id: providerId, + }, + ] as const, + (json) => [json.provider_id] as const, +); + export type KnownErrors = { [K in keyof typeof KnownErrors]: InstanceType; }; @@ -1117,6 +1143,8 @@ export const KnownErrors = { UserAuthenticationRequired, TeamMembershipAlreadyExists, TeamPermissionRequired, + InvalidSharedOAuthProviderId, + InvalidStandardOAuthProviderId, } satisfies Record>; diff --git a/packages/stack-shared/src/schema-fields.ts b/packages/stack-shared/src/schema-fields.ts index ee214cad3a..67ed50dae4 100644 --- a/packages/stack-shared/src/schema-fields.ts +++ b/packages/stack-shared/src/schema-fields.ts @@ -1,4 +1,5 @@ import * as yup from "yup"; +import { allProviders } from "./utils/oauth"; import { isUuid } from "./utils/uuids"; const _idDescription = (identify: string) => `The unique identifier of this ${identify}`; @@ -124,7 +125,7 @@ export const projectCreateTeamOnSignUpSchema = yupBoolean().meta({ openapiField: export const projectMagicLinkEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether magic link authentication is enabled for this project', exampleValue: true } }); export const projectCredentialEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether email password authentication is enabled for this project', exampleValue: true } }); // Project OAuth config -export const oauthIdSchema = yupString().oneOf(['google', 'github', 'facebook', 'microsoft', 'spotify']).meta({ openapiField: { description: 'OAuth provider ID, one of google, github, facebook, microsoft, spotify', exampleValue: 'google' } }); +export const oauthIdSchema = yupString().oneOf(allProviders).meta({ openapiField: { description: `OAuth provider ID, one of ${allProviders.map(x => `\`${x}\``).join(', ')}`, exampleValue: 'google' } }); export const oauthEnabledSchema = yupBoolean().meta({ openapiField: { description: 'Whether the OAuth provider is enabled. If an provider is first enabled, then disabled, it will be shown in the list but with enabled=false', exampleValue: true } }); export const oauthTypeSchema = yupString().oneOf(['shared', 'standard']).meta({ openapiField: { description: 'OAuth provider type, one of shared, standard. "shared" uses Stack shared OAuth keys and it is only meant for development. "standard" uses your own OAuth keys and will show your logo and company name when signing in with the provider.', exampleValue: 'standard' } }); export const oauthClientIdSchema = yupString().meta({ openapiField: { description: 'OAuth client ID. Needs to be specified when using type="standard"', exampleValue: 'google-oauth-client-id' } }); diff --git a/packages/stack-shared/src/utils/oauth.tsx b/packages/stack-shared/src/utils/oauth.tsx new file mode 100644 index 0000000000..567206bd51 --- /dev/null +++ b/packages/stack-shared/src/utils/oauth.tsx @@ -0,0 +1,7 @@ +export const standardProviders = ["google", "github", "facebook", "microsoft", "spotify"] as const; +export const sharedProviders = ["google", "github", "facebook", "microsoft"] as const; +export const allProviders = ["google", "github", "facebook", "microsoft", "spotify"] as const; + +export type ProviderType = typeof allProviders[number]; +export type StandardProviderType = typeof standardProviders[number]; +export type SharedProviderType = typeof sharedProviders[number]; diff --git a/packages/stack/src/lib/stack-app.ts b/packages/stack/src/lib/stack-app.ts index 77cf051e32..d27129d253 100644 --- a/packages/stack/src/lib/stack-app.ts +++ b/packages/stack/src/lib/stack-app.ts @@ -2,7 +2,6 @@ import { isReactServer } from "@stackframe/stack-sc"; import { KnownError, KnownErrors, StackAdminInterface, StackClientInterface, StackServerInterface } from "@stackframe/stack-shared"; import { ProductionModeError, getProductionModeErrors } from "@stackframe/stack-shared/dist/helpers/production-mode"; import { ApiKeyCreateCrudRequest, ApiKeyCreateCrudResponse } from "@stackframe/stack-shared/dist/interface/adminInterface"; -import { StandardProvider } from "@stackframe/stack-shared/dist/interface/clientInterface"; import { ApiKeysCrud } from "@stackframe/stack-shared/dist/interface/crud/api-keys"; import { CurrentUserCrud } from "@stackframe/stack-shared/dist/interface/crud/current-user"; import { EmailTemplateCrud, EmailTemplateType } from "@stackframe/stack-shared/dist/interface/crud/email-templates"; @@ -17,6 +16,7 @@ import { isBrowserLike } from "@stackframe/stack-shared/dist/utils/env"; import { StackAssertionError, throwErr } from "@stackframe/stack-shared/dist/utils/errors"; import { ReadonlyJson } from "@stackframe/stack-shared/dist/utils/json"; import { DependenciesMap } from "@stackframe/stack-shared/dist/utils/maps"; +import { ProviderType } from "@stackframe/stack-shared/dist/utils/oauth"; import { deepPlainEquals, filterUndefined, omit } from "@stackframe/stack-shared/dist/utils/objects"; import { ReactPromise, neverResolve, runAsynchronously, wait } from "@stackframe/stack-shared/dist/utils/promises"; import { suspend, suspendIfSsr } from "@stackframe/stack-shared/dist/utils/react"; @@ -73,7 +73,7 @@ export type HandlerUrls = { } export type OAuthScopesOnSignIn = { - [key in StandardProvider]: string[]; + [key in ProviderType]: string[]; }; @@ -306,7 +306,7 @@ class _StackClientAppImpl( + private readonly _currentUserOAuthConnectionCache = createCacheBySession<[ProviderType, string, boolean], OAuthConnection | null>( async (session, [connectionId, scope, redirect]) => { const user = await this._currentUserCache.getOrWait([session], "write-only"); @@ -730,16 +730,16 @@ class _StackClientAppImpl; - async function getConnectedAccount(id: StandardProvider, options: { or: 'redirect', scopes?: string[] }): Promise; - async function getConnectedAccount(id: StandardProvider, options?: { or?: 'redirect', scopes?: string[] }): Promise { + async function getConnectedAccount(id: ProviderType, options?: { scopes?: string[] }): Promise; + async function getConnectedAccount(id: ProviderType, options: { or: 'redirect', scopes?: string[] }): Promise; + async function getConnectedAccount(id: ProviderType, options?: { or?: 'redirect', scopes?: string[] }): Promise { const scopeString = options?.scopes?.join(" "); return await app._currentUserOAuthConnectionCache.getOrWait([session, id, scopeString || "", options?.or === 'redirect'], "write-only"); } - function useConnectedAccount(id: StandardProvider, options?: { scopes?: string[] }): OAuthConnection | null; - function useConnectedAccount(id: StandardProvider, options: { or: 'redirect', scopes?: string[] }): OAuthConnection; - function useConnectedAccount(id: StandardProvider, options?: { or?: 'redirect', scopes?: string[] }): OAuthConnection | null { + function useConnectedAccount(id: ProviderType, options?: { scopes?: string[] }): OAuthConnection | null; + function useConnectedAccount(id: ProviderType, options: { or: 'redirect', scopes?: string[] }): OAuthConnection; + function useConnectedAccount(id: ProviderType, options?: { or?: 'redirect', scopes?: string[] }): OAuthConnection | null { const scopeString = options?.scopes?.join(" "); return useAsyncCache(app._currentUserOAuthConnectionCache, [session, id, scopeString || "", options?.or === 'redirect'], "user.useConnectedAccount()"); } @@ -1014,7 +1014,7 @@ class _StackClientAppImpl, + getConnectedAccount(id: ProviderType, options: { or: 'redirect', scopes?: string[] }): Promise, + getConnectedAccount(id: ProviderType, options?: { or?: 'redirect' | 'throw' | 'return-null', scopes?: string[] }): Promise, + useConnectedAccount(id: ProviderType, options: { or: 'redirect', scopes?: string[] }): OAuthConnection, + useConnectedAccount(id: ProviderType, options?: { or?: 'redirect' | 'throw' | 'return-null', scopes?: string[] }): OAuthConnection | null, + hasPermission(scope: Team, permissionId: string): Promise, setSelectedTeam(team: Team | null): Promise, createTeam(data: TeamCreateOptions): Promise, - - getConnectedAccount(id: StandardProvider, options: { or: 'redirect', scopes?: string[] }): Promise, - getConnectedAccount(id: StandardProvider, options?: { or?: 'redirect' | 'throw' | 'return-null', scopes?: string[] }): Promise, - useConnectedAccount(id: StandardProvider, options: { or: 'redirect', scopes?: string[] }): OAuthConnection, - useConnectedAccount(id: StandardProvider, options?: { or?: 'redirect' | 'throw' | 'return-null', scopes?: string[] }): OAuthConnection | null, } & AsyncStoreProperty<"team", [id: string], Team | null, false> & AsyncStoreProperty<"teams", [], Team[], true>