mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 18:35:07 +03:00
This PR bumps `shadcn` and regenerates all blocks with the latest CLI. The blocks have no meaningful change (only a json property reorder). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated internal development tooling used by the UI library and Vue blocks. * **Tests** * Improved type consistency in registry-related test utilities, supporting more reliable validation without changing user-facing behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
37 lines
12 KiB
JSON
37 lines
12 KiB
JSON
{
|
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
|
"name": "oauth-consent-react",
|
|
"title": "OAuth Consent for React",
|
|
"description": "OAuth 2.1 consent component for an existing authenticated application.",
|
|
"dependencies": [
|
|
"@supabase/supabase-js@latest"
|
|
],
|
|
"registryDependencies": [
|
|
"button",
|
|
"card",
|
|
"https://supabase.com/library/r/safe-next-path.json"
|
|
],
|
|
"files": [
|
|
{
|
|
"path": "registry/default/blocks/oauth-consent/components/oauth-consent.tsx",
|
|
"content": "'use client'\n\nimport { cn } from '@/lib/utils'\nimport {\n useOAuthConsent,\n type OAuthConsentDecision,\n} from '@/registry/default/blocks/oauth-consent/hooks/use-oauth-consent'\nimport { Button } from '@/registry/default/components/ui/button'\nimport {\n Card,\n CardContent,\n CardDescription,\n CardHeader,\n CardTitle,\n} from '@/registry/default/components/ui/card'\n\nconst getInitial = (value: string) => value.trim().charAt(0).toUpperCase() || '?'\n\ninterface ConsentCardShellProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName: string\n}\n\nfunction ConsentCardShell({\n clientName,\n productName,\n className,\n children,\n ...props\n}: ConsentCardShellProps) {\n return (\n <div className={cn('flex flex-col gap-6', className)} {...props}>\n <Card>\n <CardHeader className=\"items-center space-y-4 text-center\">\n <div\n className=\"flex items-center justify-center\"\n aria-label={`${clientName} connecting to ${productName}`}\n >\n <div className=\"flex size-12 items-center justify-center rounded-full border bg-muted font-medium\">\n {getInitial(clientName)}\n </div>\n <div className=\"h-px w-8 bg-border\" aria-hidden=\"true\" />\n <div className=\"flex size-12 items-center justify-center rounded-full border bg-muted font-medium\">\n {getInitial(productName)}\n </div>\n </div>\n <div className=\"space-y-1.5\">\n <CardTitle className=\"text-2xl\">Authorize {clientName}</CardTitle>\n <CardDescription>Review what this client gets access to.</CardDescription>\n </div>\n </CardHeader>\n <CardContent className=\"space-y-6\">{children}</CardContent>\n </Card>\n </div>\n )\n}\n\nexport interface OAuthConsentCardProps extends React.ComponentPropsWithoutRef<'div'> {\n clientName: string\n productName?: string\n redirectUri: string\n email: string\n scopes?: string[]\n error?: string | null\n decision?: OAuthConsentDecision | null\n onApprove?: () => void\n onDeny?: () => void\n}\n\nexport function OAuthConsentCard({\n clientName,\n productName = 'Your product',\n redirectUri,\n email,\n scopes = [],\n error = null,\n decision = null,\n onApprove,\n onDeny,\n ...props\n}: OAuthConsentCardProps) {\n return (\n <ConsentCardShell clientName={clientName} productName={productName} {...props}>\n <dl className=\"divide-y rounded-lg border text-sm\">\n <div className=\"flex items-center justify-between gap-6 p-4\">\n <dt className=\"text-muted-foreground\">Client</dt>\n <dd className=\"min-w-0 break-all text-right font-medium\">{clientName}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-6 p-4\">\n <dt className=\"text-muted-foreground\">Redirects to</dt>\n <dd className=\"min-w-0 break-all text-right font-medium\">{redirectUri}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-6 p-4\">\n <dt className=\"text-muted-foreground\">Signed in as</dt>\n <dd className=\"min-w-0 break-all text-right font-medium\">{email}</dd>\n </div>\n {scopes.length > 0 && (\n <div className=\"flex items-center justify-between gap-6 p-4\">\n <dt className=\"text-muted-foreground\">Scopes</dt>\n <dd className=\"min-w-0 break-all text-right font-medium\">{scopes.join(', ')}</dd>\n </div>\n )}\n </dl>\n <p className=\"text-sm text-muted-foreground\">\n Allow access only if you recognize this application. It can act on your behalf only within\n the requested permissions and the access you already have.\n </p>\n {error && (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error}\n </p>\n )}\n <div className=\"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end\">\n <Button type=\"button\" variant=\"outline\" disabled={decision !== null} onClick={onDeny}>\n {decision === 'deny' ? 'Denying...' : 'Deny'}\n </Button>\n <Button type=\"button\" disabled={decision !== null} onClick={onApprove}>\n {decision === 'approve' ? 'Allowing...' : 'Allow access'}\n </Button>\n </div>\n </ConsentCardShell>\n )\n}\n\ninterface OAuthConsentProps extends React.ComponentPropsWithoutRef<'div'> {\n authorizationId?: string | null\n signInPath?: string\n productName?: string\n}\n\nexport function OAuthConsent({\n authorizationId,\n signInPath = '/auth/login',\n productName = 'Your product',\n ...props\n}: OAuthConsentProps) {\n const { details, email, error, isLoading, decision, approve, deny } = useOAuthConsent({\n authorizationId,\n signInPath,\n })\n\n if (isLoading || !details || !email) {\n return (\n <ConsentCardShell clientName=\"OAuth client\" productName={productName} {...props}>\n {isLoading ? (\n <p role=\"status\" className=\"text-sm text-muted-foreground\">\n Loading authorization request...\n </p>\n ) : (\n <p role=\"alert\" className=\"text-sm text-destructive\">\n {error ??\n 'Unable to load the authorization request. Start again from your OAuth client.'}\n </p>\n )}\n </ConsentCardShell>\n )\n }\n\n return (\n <OAuthConsentCard\n clientName={details.client.name}\n productName={productName}\n redirectUri={details.redirect_uri}\n email={email}\n scopes={details.scope.split(' ').filter(Boolean)}\n error={error}\n decision={decision}\n onApprove={() => void approve()}\n onDeny={() => void deny()}\n {...props}\n />\n )\n}\n",
|
|
"type": "registry:component"
|
|
},
|
|
{
|
|
"path": "registry/default/blocks/oauth-consent/hooks/use-oauth-consent.ts",
|
|
"content": "import { isAuthSessionMissingError } from '@supabase/supabase-js'\nimport type { OAuthAuthorizationDetails } from '@supabase/supabase-js'\nimport { useCallback, useEffect, useRef, useState } from 'react'\n\nimport { safeNextPath } from '@/registry/default/blocks/safe-next-path/lib/safe-next-path'\nimport { createClient } from '@/registry/default/clients/react/lib/supabase/client'\n\nexport type OAuthConsentDecision = 'approve' | 'deny'\n\nexport interface UseOAuthConsentOptions {\n authorizationId?: string | null\n signInPath?: string\n}\n\nconst withNextParam = (path: string, next: string) => {\n const url = new URL(path, window.location.origin)\n const searchParams = new URLSearchParams(url.search)\n searchParams.set('next', next)\n url.search = searchParams.toString()\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nconst useOAuthConsent = ({\n authorizationId,\n signInPath = '/auth/login',\n}: UseOAuthConsentOptions) => {\n const [details, setDetails] = useState<OAuthAuthorizationDetails | null>(null)\n const [error, setError] = useState<string | null>(null)\n const [isLoading, setIsLoading] = useState(true)\n const [decision, setDecision] = useState<OAuthConsentDecision | null>(null)\n const isDeciding = useRef(false)\n\n useEffect(() => {\n let active = true\n\n const loadAuthorization = async () => {\n setIsLoading(true)\n setError(null)\n setDetails(null)\n setDecision(null)\n\n if (!authorizationId) {\n setError('This page needs an authorization_id. Start again from your OAuth client.')\n setIsLoading(false)\n return\n }\n\n const supabase = createClient()\n const {\n data: { user },\n error: userError,\n } = await supabase.auth.getUser()\n\n if (userError && !isAuthSessionMissingError(userError)) {\n if (active) {\n setError(userError.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!user) {\n const next = `${window.location.pathname}${window.location.search}`\n if (active) {\n window.location.replace(withNextParam(safeNextPath(signInPath, '/auth/login'), next))\n }\n return\n }\n\n const { data, error } = await supabase.auth.oauth.getAuthorizationDetails(authorizationId)\n if (error) {\n if (active) {\n setError(error.message)\n setIsLoading(false)\n }\n return\n }\n\n if (!('authorization_id' in data)) {\n if (active) {\n window.location.replace(data.redirect_url)\n }\n return\n }\n\n if (active) {\n setDetails(data)\n setIsLoading(false)\n }\n }\n\n void loadAuthorization()\n return () => {\n active = false\n }\n }, [authorizationId, signInPath])\n\n const decide = useCallback(\n async (action: OAuthConsentDecision) => {\n if (!authorizationId || isDeciding.current) return\n\n isDeciding.current = true\n setDecision(action)\n setError(null)\n const supabase = createClient()\n const result =\n action === 'approve'\n ? await supabase.auth.oauth.approveAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n : await supabase.auth.oauth.denyAuthorization(authorizationId, {\n skipBrowserRedirect: true,\n })\n\n if (result.error) {\n setError(result.error.message)\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n if (!result.data?.redirect_url) {\n setError('The server did not return a redirect. Start again from your OAuth client.')\n setDecision(null)\n isDeciding.current = false\n return\n }\n\n window.location.assign(result.data.redirect_url)\n },\n [authorizationId]\n )\n\n return {\n details,\n email: details?.user.email ?? null,\n error,\n isLoading,\n decision,\n approve: () => decide('approve'),\n deny: () => decide('deny'),\n }\n}\n\ntype UseOAuthConsentReturn = ReturnType<typeof useOAuthConsent>\n\nexport { useOAuthConsent, type OAuthAuthorizationDetails, type UseOAuthConsentReturn }\n",
|
|
"type": "registry:hook"
|
|
},
|
|
{
|
|
"path": "registry/default/clients/react/lib/supabase/client.ts",
|
|
"content": "import { createClient as createSupabaseClient } from '@supabase/supabase-js'\n\nexport function createClient() {\n return createSupabaseClient(\n import.meta.env.VITE_SUPABASE_URL!,\n import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY!\n )\n}\n",
|
|
"type": "registry:lib"
|
|
}
|
|
],
|
|
"envVars": {
|
|
"VITE_SUPABASE_URL": "",
|
|
"VITE_SUPABASE_PUBLISHABLE_KEY": ""
|
|
},
|
|
"docs": "After installation, set the following in `supabase/config.toml`. Recent CLI versions already write an `[auth.oauth_server]` section with `enabled = false` — edit that section rather than adding a second one:\n\n```toml\n[auth.oauth_server]\nenabled = true\nauthorization_url_path = \"/oauth/consent\"\n```\n\nYou'll need to set the following environment variables in your project: `VITE_SUPABASE_URL` and `VITE_SUPABASE_PUBLISHABLE_KEY`.",
|
|
"type": "registry:block"
|
|
} |