{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "oauth-consent-nextjs",
  "type": "registry:block",
  "title": "OAuth Consent for Next.js",
  "description": "OAuth 2.1 consent screen and route for an existing authenticated application.",
  "dependencies": [
    "@supabase/ssr@latest",
    "@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/nextjs/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/blocks/oauth-consent-nextjs/app/oauth/consent/page.tsx",
      "content": "import { OAuthConsent } from '@/registry/default/blocks/oauth-consent/components/oauth-consent'\n\nexport default async function ConsentPage({\n  searchParams,\n}: {\n  searchParams: Promise<{ authorization_id?: string }>\n}) {\n  const { authorization_id } = await searchParams\n\n  return (\n    <main className=\"flex min-h-svh items-center justify-center p-6 md:p-10\">\n      <OAuthConsent className=\"w-full max-w-lg\" authorizationId={authorization_id} />\n    </main>\n  )\n}\n",
      "type": "registry:page",
      "target": "app/oauth/consent/page.tsx"
    },
    {
      "path": "registry/default/clients/nextjs/lib/supabase/client.ts",
      "content": "import { createBrowserClient } from '@supabase/ssr'\n\nexport function createClient() {\n  return createBrowserClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!\n  )\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/clients/nextjs/lib/supabase/middleware.ts",
      "content": "import { createServerClient } from '@supabase/ssr'\nimport { NextResponse, type NextRequest } from 'next/server'\n\nexport async function updateSession(request: NextRequest) {\n  let supabaseResponse = NextResponse.next({\n    request,\n  })\n\n  // With Fluid compute, don't put this client in a global environment\n  // variable. Always create a new one on each request.\n  const supabase = createServerClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n    {\n      cookies: {\n        getAll() {\n          return request.cookies.getAll()\n        },\n        setAll(cookiesToSet) {\n          cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value))\n          supabaseResponse = NextResponse.next({\n            request,\n          })\n          cookiesToSet.forEach(({ name, value, options }) =>\n            supabaseResponse.cookies.set(name, value, options)\n          )\n        },\n      },\n    }\n  )\n\n  // Do not run code between createServerClient and\n  // supabase.auth.getClaims(). A simple mistake could make it very hard to debug\n  // issues with users being randomly logged out.\n\n  // IMPORTANT: If you remove getClaims() and you use server-side rendering\n  // with the Supabase client, your users may be randomly logged out.\n  const { data } = await supabase.auth.getClaims()\n  const user = data?.claims\n\n  if (\n    !user &&\n    !request.nextUrl.pathname.startsWith('/login') &&\n    !request.nextUrl.pathname.startsWith('/auth') &&\n    // the OAuth consent route sends unauthenticated visitors to the login page\n    // itself, so that it can preserve the authorization in the `next` parameter\n    request.nextUrl.pathname !== '/oauth/consent'\n  ) {\n    // no user, potentially respond by redirecting the user to the login page\n    const url = request.nextUrl.clone()\n    url.pathname = '/auth/login'\n    return NextResponse.redirect(url)\n  }\n\n  // IMPORTANT: You *must* return the supabaseResponse object as it is.\n  // If you're creating a new response object with NextResponse.next() make sure to:\n  // 1. Pass the request in it, like so:\n  //    const myNewResponse = NextResponse.next({ request })\n  // 2. Copy over the cookies, like so:\n  //    myNewResponse.cookies.setAll(supabaseResponse.cookies.getAll())\n  // 3. Change the myNewResponse object to fit your needs, but avoid changing\n  //    the cookies!\n  // 4. Finally:\n  //    return myNewResponse\n  // If this is not done, you may be causing the browser and server to go out\n  // of sync and terminate the user's session prematurely!\n\n  return supabaseResponse\n}\n",
      "type": "registry:lib"
    },
    {
      "path": "registry/default/clients/nextjs/lib/supabase/server.ts",
      "content": "import { createServerClient } from '@supabase/ssr'\nimport { cookies } from 'next/headers'\n\n/**\n * If using Fluid compute: Don't put this client in a global variable. Always create a new client within each\n * function when using it.\n */\nexport async function createClient() {\n  const cookieStore = await cookies()\n\n  return createServerClient(\n    process.env.NEXT_PUBLIC_SUPABASE_URL!,\n    process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!,\n    {\n      cookies: {\n        getAll() {\n          return cookieStore.getAll()\n        },\n        setAll(cookiesToSet) {\n          try {\n            cookiesToSet.forEach(({ name, value, options }) =>\n              cookieStore.set(name, value, options)\n            )\n          } catch {\n            // The `setAll` method was called from a Server Component.\n            // This can be ignored if you have middleware refreshing\n            // user sessions.\n          }\n        },\n      },\n    }\n  )\n}\n",
      "type": "registry:lib"
    }
  ],
  "envVars": {
    "NEXT_PUBLIC_SUPABASE_URL": "",
    "NEXT_PUBLIC_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: `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY`."
}