diff --git a/apps/www/pages/supasquad.mdx b/apps/www/pages/supasquad.mdx index 04eee13ef51..17fa4496aef 100644 --- a/apps/www/pages/supasquad.mdx +++ b/apps/www/pages/supasquad.mdx @@ -37,7 +37,7 @@ Spread the word on social channels and help to answer Supabase-related questions ### Builder -Build Supabase examples, blog about them, and add them to the [SupaSquad GitHub Org](https://github.com/supasquad). +Build Supabase examples, blog about them, and add them to the [Supabase repo](https://github.com/supabase/supabase/tree/master/examples). ### Author @@ -59,7 +59,6 @@ Help us maintain the community guidelines in our GitHub and Community-led commun - Free credits that you can use for Squad efforts. - Direct access to members of the Supabase team for questions, suggestions, etc. - Help shape the future of the program. -- Invited to the [SupaSquad GitHub Org](https://github.com/supasquad) to collaborate with the other squad members. - Exclusive Supabase Team swag drops are usually exclusively reserved for the Supabase core team. ## How to join diff --git a/package-lock.json b/package-lock.json index 8f7cf61213f..1b914615e5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -42804,7 +42804,7 @@ "license": "MIT", "dependencies": { "@headlessui/react": "^1.7.14", - "@supabase/gotrue-js": "^2.31.0", + "@supabase/gotrue-js": "^2.35.0", "@supabase/ui": "^0.37.0-alpha.50", "react-use": "^17.4.0" }, @@ -42817,9 +42817,9 @@ } }, "packages/common/node_modules/@supabase/gotrue-js": { - "version": "2.31.0", - "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.31.0.tgz", - "integrity": "sha512-YcwlbbNfedlue/HVIXtYBb4fuOrs29gNOTl6AmyxPp4zryRxzFvslVN9kmLDBRUAVU9fnPJh2bgOR3chRjJX5w==", + "version": "2.35.0", + "resolved": "https://registry.npmjs.org/@supabase/gotrue-js/-/gotrue-js-2.35.0.tgz", + "integrity": "sha512-bLNIrBzFNRR61qUN8VZ4iV7q+fPilWyYQi0CZlxuHBiPtVcNPTul02DbNvgvw76hNz+mB5maay/Y17lSHnSRmw==", "dependencies": { "cross-fetch": "^3.1.5" } diff --git a/packages/common/gotrue.ts b/packages/common/gotrue.ts index 9df248b6197..2ef1061dfe7 100644 --- a/packages/common/gotrue.ts +++ b/packages/common/gotrue.ts @@ -1,9 +1,18 @@ import { GoTrueClient } from '@supabase/gotrue-js' export const STORAGE_KEY = process.env.NEXT_PUBLIC_STORAGE_KEY || 'supabase.dashboard.auth.token' +export const AUTH_DEBUG_KEY = + process.env.NEXT_PUBLIC_AUTH_DEBUG_KEY || 'supabase.dashboard.auth.debug' + +const debug = + process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' && + globalThis && + globalThis.localStorage && + globalThis.localStorage.getItem(AUTH_DEBUG_KEY) === 'true' export const gotrueClient = new GoTrueClient({ url: process.env.NEXT_PUBLIC_GOTRUE_URL, storageKey: STORAGE_KEY, detectSessionInUrl: true, + debug, }) diff --git a/packages/common/package.json b/packages/common/package.json index eba24a4faeb..f41a62d834e 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -6,7 +6,7 @@ "license": "MIT", "dependencies": { "@headlessui/react": "^1.7.14", - "@supabase/gotrue-js": "^2.31.0", + "@supabase/gotrue-js": "^2.35.0", "@supabase/ui": "^0.37.0-alpha.50", "react-use": "^17.4.0" }, diff --git a/packages/ui/src/components/Tabs/Tabs.tsx b/packages/ui/src/components/Tabs/Tabs.tsx index 67d389baecd..0ee08f3cc3a 100644 --- a/packages/ui/src/components/Tabs/Tabs.tsx +++ b/packages/ui/src/components/Tabs/Tabs.tsx @@ -54,6 +54,9 @@ const Tabs: React.FC & TabsSubComponents = ({ children = [children] } + // Exclude children that are just conditional elements + children = children.filter((it) => it) + const tabIds = children.map((tab) => tab.props.id) const { groupActiveId, setGroupActiveId } = useTabGroup(tabIds) diff --git a/spec/cli_v1_commands.yaml b/spec/cli_v1_commands.yaml index 42cfd39fcbf..91f0d6d6b3f 100644 --- a/spec/cli_v1_commands.yaml +++ b/spec/cli_v1_commands.yaml @@ -1,7 +1,7 @@ clispec: '001' info: id: cli - version: 1.74.0 + version: 1.75.1 title: Supabase CLI language: sh source: https://github.com/supabase/cli @@ -994,6 +994,7 @@ commands: - management-api links: [] subcommands: + - supabase-orgs-create - supabase-orgs-list flags: [] - id: supabase-orgs-list @@ -1005,6 +1006,15 @@ commands: usage: supabase orgs list subcommands: [] flags: [] + - id: supabase-orgs-create + title: supabase orgs create + summary: Create an organization + description: Create an organization for the logged-in user. + tags: [] + links: [] + usage: supabase orgs create + subcommands: [] + flags: [] - id: supabase-network-restrictions title: supabase network-restrictions summary: Manage network restrictions diff --git a/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx b/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx new file mode 100644 index 00000000000..d98c2229d8b --- /dev/null +++ b/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectButton.tsx @@ -0,0 +1,358 @@ +import { FC, useEffect, useMemo, useState } from 'react' +import { + Alert, + Button, + IconAlertCircle, + IconLoader, + IconShield, + IconTool, + IconUsers, + Listbox, + Loading, + Modal, +} from 'ui' +import * as Tooltip from '@radix-ui/react-tooltip' +import { PermissionAction } from '@supabase/shared-types/out/constants' + +import { useStore, useSelectedProject, useCheckPermissions } from 'hooks' +import { useProjectTransferPreviewQuery } from 'data/projects/project-transfer-preview-query' +import { useProjectTransferMutation } from 'data/projects/project-transfer-mutation' +import { useOrganizationsQuery } from 'data/organizations/organizations-query' +import Link from 'next/link' + +const TransferProjectButton: FC<{}> = () => { + const { ui } = useStore() + + const project = useSelectedProject() + const projectRef = project?.ref + const projectOrgId = project?.organization_id + const { data: allOrganizations } = useOrganizationsQuery() + + const organizations = (allOrganizations || []) + .filter((it) => it.id !== projectOrgId) + // Only orgs with org-level subscription + .filter((it) => it.subscription_id) + + const [isOpen, setIsOpen] = useState(false) + const [isLoading, setIsLoading] = useState(false) + + const [selectedOrg, setSelectedOrg] = useState() + + const { error: transferError, mutateAsync: transferProject } = useProjectTransferMutation() + + const { + data: transferPreviewData, + error: transferPreviewError, + isLoading: transferPreviewIsLoading, + remove, + } = useProjectTransferPreviewQuery( + { projectRef, targetOrganizationSlug: selectedOrg }, + { enabled: !isLoading && isOpen } + ) + + useEffect(() => { + if (isOpen) { + // reset state + setSelectedOrg(undefined) + setIsLoading(false) + } else { + // Invalidate cache + remove() + } + }, [isOpen]) + + const canTransferProject = useCheckPermissions(PermissionAction.UPDATE, 'projects') + + const toggle = () => { + setIsOpen(!isOpen) + } + + async function handleTransferProject() { + if (project === undefined) return + if (selectedOrg === undefined) return + + setIsLoading(true) + + try { + await transferProject({ projectRef, targetOrganizationSlug: selectedOrg }) + ui.setNotification({ + category: 'success', + + duration: 5000, + message: `Successfully transferred project ${project?.name}.`, + }) + setIsOpen(false) + } catch { + } finally { + setIsLoading(false) + } + } + + return ( + <> + + + + + {!canTransferProject && ( + + + +
+ + You need additional permissions to delete this project + +
+
+
+ )} +
+ + toggle()} + visible={isOpen} + loading={isLoading} + size={'xlarge'} + header={`Transfer project ${project?.name}`} + customFooter={ +
+ + +
+ } + > +
+ +

+ To transfer projects, the owner must be a member of both the source and target + organizations. +

+ +

Transferring considerations:

+ +
    +
  • + + + +
    +

    No downtime

    +

    There is no downtime or restrictions involved when transferring a project.

    +
    +
  • + +
  • + + + +
    +

    Permissions

    +

    + Depending on your role in the target organization, your level of permissions may + change after transfer. +

    +
    +
  • + +
  • + + + +
    +

    Features

    +

    + Moving your project to an organization with a smaller subscription plan may + result in the loss of certain features (i.e. image transformations). +

    +
    +
  • +
+
+ + {organizations && ( +
+ {organizations.length === 0 ? ( +
+ You do not have any organizations with an organization-level + subscription. +
+ ) : ( + setSelectedOrg(slug)} + placeholder="Select Organization" + > + + Select Organization + + {organizations.map((x: any) => ( + } + > + {x.name} + + ))} + + )} + +

+ The target organization needs to use{' '} + + + organization-level-billing + + + . To migrate an organization to the new billing, head to your{' '} + + + organizations settings + + + . +

+
+ )} +
+ + + +
+ {transferPreviewData && transferPreviewData.valid && ( +
+ {transferPreviewData.source_subscription_plan !== + transferPreviewData.target_subscription_plan ? ( +
+

+ Your project is currently on the{' '} + {transferPreviewData.source_subscription_plan} plan, whereas the target + organization uses the {transferPreviewData.target_subscription_plan} plan. +

+
+ ) : ( +
+ Your project and the target organization are both on the{' '} + {transferPreviewData.source_subscription_plan} subscription plan. +
+ )} + +
+ {transferPreviewData.credits_on_source_organization === 0 ? ( + + {' '} + Your current organization won't be granted any prorated credits. + + ) : ( + + {' '} + Your current organization will be granted{' '} + + ${transferPreviewData.credits_on_source_organization} + {' '} + in credits as proration. + + )} + {transferPreviewData.costs_on_target_organization === 0 ? ( + + {' '} + The target organization won't be charged any immediate upfront payment. + + ) : ( + + {' '} + The target organization will be billed{' '} + + ${transferPreviewData.costs_on_target_organization} + {' '} + immediately to prorate for the remainder of the billing period. + + )} +
+
+ )} + {transferPreviewData && transferPreviewData.warnings.length > 0 && ( + +
+ {transferPreviewData.warnings.map((warning) => ( +

{warning.message}

+ ))} +
+
+ )} + {transferPreviewData && transferPreviewData.errors.length > 0 && ( + +
+ {transferPreviewData.errors.map((error) => ( +

{error.message}

+ ))} +
+ {transferPreviewData.members_exceeding_free_project_limit.length > 0 && ( +
+

+ These members have reached their maximum limits for the number of active + Free plan projects within organizations where they are an administrator or + owner: +

+
    + {(transferPreviewData.members_exceeding_free_project_limit || []).map( + (member, idx: number) => ( +
  • + {member.name} (Limit: {member.limit} free projects) +
  • + ) + )} +
+

+ These members will need to either delete, pause, or upgrade one or more of + their projects before you can downgrade this project. +

+
+ )} +
+ )} + {transferPreviewError && !transferError && ( + +

{transferPreviewError.message}

+
+ )} + {transferError && ( + +

{transferError.message}

+
+ )} +
+
+
+
+
+ + ) +} + +export default TransferProjectButton diff --git a/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectPanel.tsx b/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectPanel.tsx new file mode 100644 index 00000000000..ff8ce40428d --- /dev/null +++ b/studio/components/interfaces/Settings/General/TransferProjectPanel/TransferProjectPanel.tsx @@ -0,0 +1,46 @@ +import { FC } from 'react' + +import { useSelectedProject } from 'hooks' +import Panel from 'components/ui/Panel' + +import TransferProjectButton from './TransferProjectButton' +import { FormHeader } from 'components/ui/Forms' +import { IconTruck } from 'ui' + +interface Props {} + +const TransferProjectPanel: FC = ({}) => { + const project = useSelectedProject() + + if (project === undefined) return <> + + return ( +
+ + + +
+
+ +
+

Transfer project to another organization

+

+ To transfer projects, the owner must be a member of both the source and target + organizations. +

+
+
+
+ +
+
+
+
+
+ ) +} + +export default TransferProjectPanel diff --git a/studio/components/interfaces/Settings/General/index.ts b/studio/components/interfaces/Settings/General/index.ts index 8ff5b36ea73..ecd3345f01c 100644 --- a/studio/components/interfaces/Settings/General/index.ts +++ b/studio/components/interfaces/Settings/General/index.ts @@ -3,5 +3,15 @@ import Infrastructure from './Infrastructure/Infrastructure' import CustomDomainConfig from './CustomDomainConfig/CustomDomainConfig' import DeleteProjectPanel from './DeleteProjectPanel/DeleteProjectPanel' import DeleteProjectButton from './DeleteProjectPanel/DeleteProjectButton' +import TransferProjectButton from './TransferProjectPanel/TransferProjectButton' +import TransferProjectPanel from './TransferProjectPanel/TransferProjectPanel' -export { General, Infrastructure, CustomDomainConfig, DeleteProjectPanel, DeleteProjectButton } +export { + General, + Infrastructure, + CustomDomainConfig, + DeleteProjectPanel, + DeleteProjectButton, + TransferProjectPanel, + TransferProjectButton, +} diff --git a/studio/components/ui/UpgradeToPro.tsx b/studio/components/ui/UpgradeToPro.tsx index e1771980505..fe0a055afa9 100644 --- a/studio/components/ui/UpgradeToPro.tsx +++ b/studio/components/ui/UpgradeToPro.tsx @@ -26,7 +26,6 @@ const UpgradeToPro: FC = ({ icon, primaryText, projectRef, secondaryText 'stripe.subscriptions' ) const projectUpdateDisabled = useFlag('disableProjectCreationAndUpdate') - const isEnterprise = tier === PRICING_TIER_PRODUCT_IDS.ENTERPRISE return (
= ({ icon, primaryText, projectRef, secondaryText diff --git a/studio/data/projects/keys.ts b/studio/data/projects/keys.ts index 0726abeaf9e..8e3e5c10db0 100644 --- a/studio/data/projects/keys.ts +++ b/studio/data/projects/keys.ts @@ -1,4 +1,10 @@ export const projectKeys = { list: () => ['all-projects'] as const, detail: (projectRef: string | undefined) => ['project', projectRef, 'detail'] as const, + projectTransfer: (projectRef: string | undefined, targetOrganizationSlug: string | undefined) => + ['projects', 'transfer', projectRef, targetOrganizationSlug] as const, + projectTransferPreview: ( + projectRef: string | undefined, + targetOrganizationSlug: string | undefined + ) => ['projects', 'transfer', projectRef, targetOrganizationSlug, 'preview'] as const, } diff --git a/studio/data/projects/project-transfer-mutation.ts b/studio/data/projects/project-transfer-mutation.ts new file mode 100644 index 00000000000..45cfe5abdcc --- /dev/null +++ b/studio/data/projects/project-transfer-mutation.ts @@ -0,0 +1,60 @@ +import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query' +import { post } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { projectKeys } from './keys' + +export type ProjectTransferVariables = { + projectRef?: string + targetOrganizationSlug?: string +} + +type ProjectTransferError = { + message: string +} + +export async function transferProject({ + projectRef, + targetOrganizationSlug, +}: ProjectTransferVariables) { + if (!projectRef) throw new Error('projectRef is required') + if (!targetOrganizationSlug) throw new Error('targetOrganizationSlug is required') + + const payload: { target_organization_slug: string } = { + target_organization_slug: targetOrganizationSlug, + } + + const response = await post(`${API_URL}/projects/${projectRef}/transfer`, payload) + if (response.error) throw response.error + + return response +} + +type ProjectTransferData = Awaited> + +export const useProjectTransferMutation = ({ + onSuccess, + ...options +}: Omit< + UseMutationOptions, + 'mutationFn' +> = {}) => { + const queryClient = useQueryClient() + + return useMutation( + (vars) => transferProject(vars), + { + async onSuccess(data, variables, context) { + const { projectRef, targetOrganizationSlug } = variables + await Promise.all([ + queryClient.invalidateQueries( + projectKeys.projectTransferPreview(projectRef, targetOrganizationSlug) + ), + queryClient.invalidateQueries(projectKeys.detail(projectRef)), + queryClient.invalidateQueries(projectKeys.list()), + ]) + await onSuccess?.(data, variables, context) + }, + ...options, + } + ) +} diff --git a/studio/data/projects/project-transfer-preview-query.ts b/studio/data/projects/project-transfer-preview-query.ts new file mode 100644 index 00000000000..a5edae66caf --- /dev/null +++ b/studio/data/projects/project-transfer-preview-query.ts @@ -0,0 +1,123 @@ +import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query' +import { post } from 'lib/common/fetch' +import { API_URL } from 'lib/constants' +import { useCallback } from 'react' +import { projectKeys } from './keys' + +export type ProjectTransferPreviewVariables = { + projectRef?: string + targetOrganizationSlug?: string +} + +export type PlanId = 'free' | 'pro' | 'team' | 'enterprise' + +type MemberExceedingFreeProjectLimit = { + name: string + limit: number +} + +type PreviewTransferInfo = { + key: string + message: string +} + +export type PreviewProjectTransferResponse = { + valid: boolean + + warnings: PreviewTransferInfo[] + errors: PreviewTransferInfo[] + + members_exceeding_free_project_limit: MemberExceedingFreeProjectLimit[] + + has_permissions_on_source_organization: boolean + has_access_to_target_organization: boolean + + source_project_eligible: boolean + + target_organization_eligible: boolean | null + target_organization_has_free_project_slots: boolean | null + + credits_on_source_organization: number + costs_on_target_organization: number + charge_on_target_organization: number + + source_subscription_plan: PlanId + target_subscription_plan: PlanId | null +} + +export async function previewProjectTransfer( + { projectRef, targetOrganizationSlug }: ProjectTransferPreviewVariables, + signal?: AbortSignal +) { + if (!projectRef) throw new Error('projectRef is required') + if (!targetOrganizationSlug) throw new Error('targetOrganizationSlug is required') + + const response = await post( + `${API_URL}/projects/${projectRef}/transfer/preview`, + { + target_organization_slug: targetOrganizationSlug, + }, + { signal } + ) + if (response.error) throw response.error + + return response as PreviewProjectTransferResponse +} + +export type ProjectTransferPreviewData = Awaited> +export type ProjectTransferPreviewError = { + message: string +} + +export const useProjectTransferPreviewQuery = ( + { projectRef, targetOrganizationSlug }: ProjectTransferPreviewVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => + useQuery( + projectKeys.projectTransferPreview(projectRef, targetOrganizationSlug), + ({ signal }) => previewProjectTransfer({ projectRef, targetOrganizationSlug }, signal), + { + enabled: + enabled && + typeof projectRef !== 'undefined' && + typeof targetOrganizationSlug !== 'undefined', + ...options, + + retry: (failureCount, error) => { + // Don't retry on 400s + if ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error as any).code === 400 + ) { + return false + } + + if (failureCount < 3) { + return true + } + + return false + }, + } + ) + +export const useProjectTransferPreviewPrefetch = ({ + projectRef, + targetOrganizationSlug, +}: ProjectTransferPreviewVariables) => { + const client = useQueryClient() + + return useCallback(() => { + if (projectRef && targetOrganizationSlug) { + client.prefetchQuery( + projectKeys.projectTransferPreview(projectRef, targetOrganizationSlug), + ({ signal }) => previewProjectTransfer({ projectRef, targetOrganizationSlug }, signal) + ) + } + }, [projectRef, targetOrganizationSlug]) +} diff --git a/studio/lib/constants/infrastructure.ts b/studio/lib/constants/infrastructure.ts index e6d02193b3d..f86304825c6 100644 --- a/studio/lib/constants/infrastructure.ts +++ b/studio/lib/constants/infrastructure.ts @@ -27,7 +27,7 @@ export const FLY_REGIONS = { } as const export const AWS_REGIONS_DEFAULT = - process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' ? AWS_REGIONS.SOUTHEAST_ASIA : AWS_REGIONS.EAST_US + process.env.NEXT_PUBLIC_ENVIRONMENT !== 'prod' ? AWS_REGIONS.SOUTHEAST_ASIA : AWS_REGIONS.WEST_US // TO DO, change default to US region for prod export const FLY_REGIONS_DEFAULT = FLY_REGIONS.SOUTHEAST_ASIA diff --git a/studio/lib/local-storage.ts b/studio/lib/local-storage.ts index d9747df8753..ce3d2b3f271 100644 --- a/studio/lib/local-storage.ts +++ b/studio/lib/local-storage.ts @@ -5,6 +5,7 @@ export const LOCAL_STORAGE_KEYS_ALLOWLIST = [ 'theme', 'supabaseDarkMode', 'supabase.dashboard.sign_in_clicks', + 'supabase.dashboard.auth.debug', ] export function clearLocalStorage() { diff --git a/studio/pages/api/auth/[ref]/config.ts b/studio/pages/api/auth/[ref]/config.ts index 66545dba0ae..80f655a588d 100644 --- a/studio/pages/api/auth/[ref]/config.ts +++ b/studio/pages/api/auth/[ref]/config.ts @@ -74,6 +74,9 @@ const handleGetAll = async (req: NextApiRequest, res: NextApiResponse) => { EXTERNAL_GOOGLE_ENABLED: false, EXTERNAL_GOOGLE_CLIENT_ID: null, EXTERNAL_GOOGLE_SECRET: null, + EXTERNAL_KAKAO_ENABLED: false, + EXTERNAL_KAKAO_CLIENT_ID: null, + EXTERNAL_KAKAO_SECRET: null, EXTERNAL_KEYCLOAK_ENABLED: false, EXTERNAL_KEYCLOAK_CLIENT_ID: null, EXTERNAL_KEYCLOAK_SECRET: null, diff --git a/studio/pages/project/[ref]/settings/general.tsx b/studio/pages/project/[ref]/settings/general.tsx index a1981c1e607..9049ec621ac 100644 --- a/studio/pages/project/[ref]/settings/general.tsx +++ b/studio/pages/project/[ref]/settings/general.tsx @@ -7,15 +7,20 @@ import { Infrastructure, CustomDomainConfig, DeleteProjectPanel, + TransferProjectPanel, } from 'components/interfaces/Settings/General' +import { useFlag } from 'hooks' const ProjectSettings: NextPageWithLayout = () => { + const transferProjectEnabled = useFlag('transferProject') + // [Joshen] Opting for larger gap instead of gap-8 as compared to other pages for better grouping of content return (
+ {transferProjectEnabled && }
) diff --git a/studio/public/img/icons/kakao-icon.svg b/studio/public/img/icons/kakao-icon.svg new file mode 100644 index 00000000000..e721ec2b432 --- /dev/null +++ b/studio/public/img/icons/kakao-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx index 3020af6abec..48eefc8e24e 100644 --- a/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx +++ b/studio/stores/authConfig/schema/AuthProviders/AuthProvidersFormValidation.tsx @@ -784,6 +784,46 @@ const EXTERNAL_PROVIDER_GOOGLE = { }, } +const EXTERNAL_PROVIDER_KAKAO = { + $schema: JSON_SCHEMA_VERSION, + type: 'object', + title: 'Kakao', + properties: { + EXTERNAL_KAKAO_ENABLED: { + title: 'Kakao enabled', + type: 'boolean', + }, + // [TODO] Update docs + EXTERNAL_KAKAO_CLIENT_ID: { + title: 'REST API Key', + type: 'string', + }, + // [TODO] Update docs + EXTERNAL_KAKAO_SECRET: { + title: 'Client Secret Code', + type: 'string', + isSecret: true, + }, + }, + validationSchema: object().shape({ + EXTERNAL_KAKAO_ENABLED: boolean().required(), + EXTERNAL_KAKAO_CLIENT_ID: string().when('EXTERNAL_KAKAO_ENABLED', { + is: true, + then: (schema) => schema.required('REST API Key is required'), + otherwise: (schema) => schema, + }), + EXTERNAL_KAKAO_SECRET: string().when('EXTERNAL_KAKAO_ENABLED', { + is: true, + then: (schema) => schema.required('Client Secret Code is required'), + otherwise: (schema) => schema, + }), + }), + misc: { + iconKey: 'kakao-icon', + requiresRedirect: true, + }, +} + // [TODO]: clarify the EXTERNAL_KEYCLOAK_URL property const EXTERNAL_PROVIDER_KEYCLOAK = { $schema: JSON_SCHEMA_VERSION, @@ -1183,6 +1223,7 @@ export const PROVIDERS_SCHEMAS = [ EXTERNAL_PROVIDER_GITHUB, EXTERNAL_PROVIDER_GITLAB, EXTERNAL_PROVIDER_GOOGLE, + EXTERNAL_PROVIDER_KAKAO, EXTERNAL_PROVIDER_KEYCLOAK, EXTERNAL_PROVIDER_LINKEDIN, EXTERNAL_PROVIDER_NOTION, diff --git a/studio/stores/jsonSchema/auth_gotrue_config.json b/studio/stores/jsonSchema/auth_gotrue_config.json index 55496508ce3..e190bc15bac 100644 --- a/studio/stores/jsonSchema/auth_gotrue_config.json +++ b/studio/stores/jsonSchema/auth_gotrue_config.json @@ -17,6 +17,7 @@ "EXTERNAL_GITHUB_ENABLED", "EXTERNAL_GITLAB_ENABLED", "EXTERNAL_GOOGLE_ENABLED", + "EXTERNAL_KAKAO_ENABLED", "EXTERNAL_KEYCLOAK_ENABLED", "EXTERNAL_LINKEDIN_ENABLED", "EXTERNAL_NOTION_ENABLED", @@ -196,6 +197,18 @@ "title": "Google secret", "type": "string" }, + "EXTERNAL_KAKAO_ENABLED": { + "title": "Kakao enabled", + "type": "boolean" + }, + "EXTERNAL_KAKAO_CLIENT_ID": { + "title": "Kakao client ID", + "type": "string" + }, + "EXTERNAL_KAKAO_SECRET": { + "title": "Kakao secret", + "type": "string" + }, "EXTERNAL_KEYCLOAK_ENABLED": { "title": "Keycloak enabled", "type": "boolean"