6ac9679fb5
* feat(auth): base login fields on GoTrue providers Signed-off-by: Goostaf <gasplund2@gmail.com> # Conflicts: # app/(auth)/login/login-client.tsx # app/(auth)/register/page.tsx * fix: address feedback Signed-off-by: Goostaf <gasplund2@gmail.com> * chore: remove hardcoded Google enabled checks Signed-off-by: Goostaf <gasplund2@gmail.com> # Conflicts: # .env.example * feat: show label when password login is disabled Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: use MicrosoftMark, correct comment Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: display custom providers Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: show when no methods are available Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: display custom provider labels Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: add SAML login path Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: show when no methods are available Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: display SAML button when enabled Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: preserve nextPath and broken key Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: redirect test to client Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: expose registerEnabled Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: provider allowlist and request timeout Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: restore compact labels Signed-off-by: Goostaf <gasplund2@gmail.com> * refactor: move withTimeout implementation to utils Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: include nextPath Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: export function and test case Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: only show SAML button if vars configured Signed-off-by: Goostaf <gasplund2@gmail.com> * fix(auth): map SAML sign-in error through getErrorMessage The antipattern ratchet (check:guards, raw-user-error) rejects a raw error.message reaching a user-visible sink. Route the signInWithSSO error through getErrorMessage like the other auth error paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013BAzJjXQBa9F5L1U42wUMj Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> * feat(auth): GitHub brand mark on the provider button; decision log GitHub allows its invertocat in solid black/white, so currentColor is correct; custom OIDC providers keep the generic key icon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013BAzJjXQBa9F5L1U42wUMj Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> --------- Signed-off-by: Goostaf <gasplund2@gmail.com> Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.4 KiB
TypeScript
99 lines
3.4 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useLocale, useTranslations } from 'next-intl'
|
|
import { createClient } from '@/lib/supabase/client'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Loader2, KeyRound } from 'lucide-react'
|
|
import { getErrorMessage, type ErrorLocale } from '@/lib/errors/get-error-message'
|
|
import { GitHubMark, GoogleMark, MicrosoftMark } from '@/components/ui/provider-marks'
|
|
import type { ResolvedProvider } from '@/lib/auth/gotrue-providers'
|
|
|
|
/**
|
|
* Render the brand mark for a known provider, or a generic key icon for
|
|
* custom OIDC providers.
|
|
*/
|
|
function ProviderMark({ provider }: { provider: ResolvedProvider }) {
|
|
if (provider.id === 'google') return <GoogleMark />
|
|
else if (provider.id === 'azure') return <MicrosoftMark />
|
|
else if (provider.id === 'github') return <GitHubMark />
|
|
else return <KeyRound className="h-4 w-4 text-muted-foreground" />
|
|
}
|
|
|
|
/**
|
|
* Generic OAuth login button. Works with any Supabase GoTrue provider.
|
|
*
|
|
* For known providers (Google, GitHub, etc.) the button shows the brand
|
|
* name; for custom OIDC providers it shows "Sign in with SSO" style text.
|
|
*
|
|
* Kicks off the Supabase OAuth redirect, with flow=oauth so
|
|
* /auth/callback can tag failures.
|
|
*/
|
|
export function OAuthButton({
|
|
provider,
|
|
onError,
|
|
compact = false,
|
|
next,
|
|
}: {
|
|
provider: ResolvedProvider
|
|
onError: (message: string) => void
|
|
compact?: boolean
|
|
/**
|
|
* Post-auth destination, already passed through safeReturnTo by the caller.
|
|
* Forwarded to /auth/callback as `next` so an OAuth sign-in or sign-up that
|
|
* started from the MCP consent page (/login?next=/api/mcp-oauth/authorize…)
|
|
* resumes the consent flow instead of landing on the dashboard. '/' (the
|
|
* safeReturnTo fallback) means no destination and is not forwarded.
|
|
*/
|
|
next?: string
|
|
}) {
|
|
const [isRedirecting, setIsRedirecting] = useState(false)
|
|
const supabase = createClient()
|
|
const tAuth = useTranslations('auth')
|
|
const errorLocale = useLocale() as ErrorLocale
|
|
|
|
const handleClick = async () => {
|
|
setIsRedirecting(true)
|
|
try {
|
|
const callback = new URL('/auth/callback', window.location.origin)
|
|
callback.searchParams.set('flow', 'oauth')
|
|
if (next && next !== '/') callback.searchParams.set('next', next)
|
|
const { error } = await supabase.auth.signInWithOAuth({
|
|
provider: provider.id as Parameters<typeof supabase.auth.signInWithOAuth>[0]['provider'],
|
|
options: {
|
|
redirectTo: callback.toString(),
|
|
},
|
|
})
|
|
if (error) {
|
|
onError(getErrorMessage(error, { context: 'auth', locale: errorLocale }))
|
|
setIsRedirecting(false)
|
|
}
|
|
} catch (error) {
|
|
onError(getErrorMessage(error, { context: 'auth', locale: errorLocale }))
|
|
setIsRedirecting(false)
|
|
}
|
|
}
|
|
|
|
const label = tAuth('continue_with_provider', { provider: provider.label })
|
|
|
|
return (
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className={compact ? 'h-10 w-full gap-2' : 'w-full h-11'}
|
|
onClick={handleClick}
|
|
disabled={isRedirecting}
|
|
aria-label={label}
|
|
>
|
|
{isRedirecting ? (
|
|
<Loader2 className={compact ? 'h-4 w-4 animate-spin' : 'mr-2 h-4 w-4 animate-spin'} />
|
|
) : (
|
|
<span className={compact ? 'flex items-center' : 'mr-2 flex items-center'}>
|
|
<ProviderMark provider={provider} />
|
|
</span>
|
|
)}
|
|
{compact ? provider.label : label}
|
|
</Button>
|
|
)
|
|
}
|