chore: customizable dashboard auth (#38516)

* chore: customizable dashboard auth

* minor fixes

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Alaister Young
2025-09-09 09:04:27 +02:00
committed by GitHub
parent ce3f7c8e81
commit 774825c1cc
11 changed files with 192 additions and 43 deletions

View File

@@ -0,0 +1,48 @@
import * as Sentry from '@sentry/nextjs'
import { useState } from 'react'
import { toast } from 'sonner'
import { BASE_PATH } from 'lib/constants'
import { auth, buildPathWithParams } from 'lib/gotrue'
import { Button } from 'ui'
interface SignInWithCustomProps {
providerName: string
}
export const SignInWithCustom = ({ providerName }: SignInWithCustomProps) => {
const [loading, setLoading] = useState(false)
async function handleCustomSignIn() {
setLoading(true)
try {
// redirects to /sign-in to check if the user has MFA setup (handled in SignInLayout.tsx)
const redirectTo = buildPathWithParams(
`${
process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview'
? location.origin
: process.env.NEXT_PUBLIC_SITE_URL
}${BASE_PATH}/sign-in-mfa?method=${providerName.toLowerCase()}`
)
const { error } = await auth.signInWithOAuth({
// @ts-expect-error - providerName is a string
provider: providerName.toLowerCase(),
options: { redirectTo },
})
if (error) throw error
} catch (error: any) {
toast.error(`Failed to sign in via ${providerName}: ${error.message}`)
Sentry.captureMessage('[CRITICAL] Failed to sign in via GH: ' + error.message)
setLoading(false)
}
}
return (
<Button block onClick={handleCustomSignIn} size="large" type="default" loading={loading}>
Continue with {providerName}
</Button>
)
}

View File

@@ -1,11 +1,17 @@
import Link from 'next/link'
import { Button } from 'ui'
import { Button, cn } from 'ui'
import { Admonition } from 'ui-patterns'
export const UnknownInterface = ({ urlBack }: { urlBack: string }) => {
export const UnknownInterface = ({
urlBack,
fullHeight = true,
}: {
urlBack: string
fullHeight?: boolean
}) => {
return (
<div className="w-full h-full flex items-center justify-center">
<div className={cn('w-full flex items-center justify-center', fullHeight && 'h-full')}>
<Admonition
type="note"
className="max-w-xl"

View File

@@ -2,6 +2,8 @@ import { CONNECTION_TYPES } from 'components/interfaces/Connect/Connect.constant
import type { CloudProvider } from 'shared-data'
export type CustomContentTypes = {
dashboardAuthCustomProvider: string
organizationLegalDocuments: {
id: string
name: string

View File

@@ -1,6 +1,8 @@
{
"$schema": "./custom-content.schema.json",
"dashboard_auth:custom_provider": null,
"organization:legal_documents": null,
"project_homepage:example_projects": null,

View File

@@ -1,6 +1,8 @@
{
"$schema": "./custom-content.schema.json",
"dashboard_auth:custom_provider": "Nimbus",
"organization:legal_documents": [
{
"id": "doc1",

View File

@@ -6,6 +6,11 @@
"type": "string"
},
"dashboard_auth:custom_provider": {
"type": ["string", "null"],
"description": "Show a custom provider on the sign in page (Continue with X)"
},
"organization:legal_documents": {
"type": ["array", "null"],
"description": "Renders a provided set of documents under the organization legal documents page",

View File

@@ -1,8 +1,16 @@
import { SignInSSOForm } from 'components/interfaces/SignIn/SignInSSOForm'
import SignInLayout from 'components/layouts/SignInLayout/SignInLayout'
import { UnknownInterface } from 'components/ui/UnknownInterface'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import type { NextPageWithLayout } from 'types'
const SignInSSOPage: NextPageWithLayout = () => {
const signInWithSSOEnabled = useIsFeatureEnabled('dashboard_auth:sign_in_with_sso')
if (!signInWithSSOEnabled) {
return <UnknownInterface fullHeight={false} urlBack="/sign-in" />
}
return (
<>
<div className="flex flex-col gap-5">

View File

@@ -5,9 +5,12 @@ import { useEffect } from 'react'
import { LastSignInWrapper } from 'components/interfaces/SignIn/LastSignInWrapper'
import { SignInForm } from 'components/interfaces/SignIn/SignInForm'
import { SignInWithCustom } from 'components/interfaces/SignIn/SignInWithCustom'
import { SignInWithGitHub } from 'components/interfaces/SignIn/SignInWithGitHub'
import { AuthenticationLayout } from 'components/layouts/AuthenticationLayout'
import SignInLayout from 'components/layouts/SignInLayout/SignInLayout'
import { useCustomContent } from 'hooks/custom-content/useCustomContent'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { IS_PLATFORM } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
import { Button } from 'ui'
@@ -15,6 +18,25 @@ import { Button } from 'ui'
const SignInPage: NextPageWithLayout = () => {
const router = useRouter()
const {
dashboardAuthSignInWithGithub: signInWithGithubEnabled,
dashboardAuthSignInWithSso: signInWithSsoEnabled,
dashboardAuthSignInWithEmail: signInWithEmailEnabled,
dashboardAuthSignUp: signUpEnabled,
} = useIsFeatureEnabled([
'dashboard_auth:sign_in_with_github',
'dashboard_auth:sign_in_with_sso',
'dashboard_auth:sign_in_with_email',
'dashboard_auth:sign_up',
])
const { dashboardAuthCustomProvider: customProvider } = useCustomContent([
'dashboard_auth:custom_provider',
])
const showOrDivider =
(signInWithGithubEnabled || signInWithSsoEnabled || customProvider) && signInWithEmailEnabled
useEffect(() => {
if (!IS_PLATFORM) {
// on selfhosted instance just redirect to projects page
@@ -25,45 +47,58 @@ const SignInPage: NextPageWithLayout = () => {
return (
<>
<div className="flex flex-col gap-5">
<SignInWithGitHub />
<LastSignInWrapper type="sso">
<Button asChild block size="large" type="outline" icon={<Lock width={18} height={18} />}>
{customProvider && <SignInWithCustom providerName={customProvider} />}
{signInWithGithubEnabled && <SignInWithGitHub />}
{signInWithSsoEnabled && (
<LastSignInWrapper type="sso">
<Button
asChild
block
size="large"
type="outline"
icon={<Lock width={18} height={18} />}
>
<Link
href={{
pathname: '/sign-in-sso',
query: router.query,
}}
>
Continue with SSO
</Link>
</Button>
</LastSignInWrapper>
)}
{showOrDivider && (
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-strong" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 text-sm bg-studio text-foreground">or</span>
</div>
</div>
)}
{signInWithEmailEnabled && <SignInForm />}
</div>
{signUpEnabled && (
<div className="self-center my-8 text-sm">
<div>
<span className="text-foreground-light">Don't have an account?</span>{' '}
<Link
href={{
pathname: '/sign-in-sso',
pathname: '/sign-up',
query: router.query,
}}
className="underline transition text-foreground hover:text-foreground-light"
>
Continue with SSO
Sign Up Now
</Link>
</Button>
</LastSignInWrapper>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-strong" />
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 text-sm bg-studio text-foreground">or</span>
</div>
</div>
<SignInForm />
</div>
<div className="self-center my-8 text-sm">
<div>
<span className="text-foreground-light">Don't have an account?</span>{' '}
<Link
href={{
pathname: '/sign-up',
query: router.query,
}}
className="underline transition text-foreground hover:text-foreground-light"
>
Sign Up Now
</Link>
</div>
</div>
)}
</>
)
}

View File

@@ -3,22 +3,37 @@ import Link from 'next/link'
import { SignInWithGitHub } from 'components/interfaces/SignIn/SignInWithGitHub'
import { SignUpForm } from 'components/interfaces/SignIn/SignUpForm'
import SignInLayout from 'components/layouts/SignInLayout/SignInLayout'
import { UnknownInterface } from 'components/ui/UnknownInterface'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import type { NextPageWithLayout } from 'types'
const SignUpPage: NextPageWithLayout = () => {
const {
dashboardAuthSignUp: signUpEnabled,
dashboardAuthSignInWithGithub: signInWithGithubEnabled,
} = useIsFeatureEnabled(['dashboard_auth:sign_up', 'dashboard_auth:sign_in_with_github'])
if (!signUpEnabled) {
return <UnknownInterface fullHeight={false} urlBack="/sign-in" />
}
return (
<>
<div className="flex flex-col gap-5">
<SignInWithGitHub />
{signInWithGithubEnabled && (
<>
<SignInWithGitHub />
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-strong" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-studio px-2 text-sm text-foreground">or</span>
</div>
</div>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-strong" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-studio px-2 text-sm text-foreground">or</span>
</div>
</div>
</>
)}
<SignUpForm />
</div>

View File

@@ -24,6 +24,11 @@
"billing:all": true,
"dashboard_auth:sign_up": true,
"dashboard_auth:sign_in_with_github": true,
"dashboard_auth:sign_in_with_sso": true,
"dashboard_auth:sign_in_with_email": true,
"database:replication": true,
"database:roles": true,

View File

@@ -86,6 +86,23 @@
"description": "Enable the billing settings page"
},
"dashboard_auth:sign_up": {
"type": "boolean",
"description": "Enable the sign up page in the dashboard"
},
"dashboard_auth:sign_in_with_github": {
"type": "boolean",
"description": "Enable the sign in with github provider"
},
"dashboard_auth:sign_in_with_sso": {
"type": "boolean",
"description": "Enable the sign in with sso provider"
},
"dashboard_auth:sign_in_with_email": {
"type": "boolean",
"description": "Enable the sign in with email/password provider"
},
"database:replication": {
"type": "boolean",
"description": "Enable the database replication page"
@@ -235,6 +252,10 @@
"authentication:show_sort_by_phone",
"authentication:show_user_type_filter",
"billing:all",
"dashboard_auth:sign_up",
"dashboard_auth:sign_in_with_github",
"dashboard_auth:sign_in_with_sso",
"dashboard_auth:sign_in_with_email",
"database:replication",
"database:roles",
"docs:self-hosting",