Ver código fonte

[Auth] /logout route + pin Supabase cookie name to NEXT_PUBLIC URL

- Add GET/POST /logout route handler — calls signOut and 307s to /.
- Rewrite /api/auth/signout to attach Set-Cookie clears to the actual
  outgoing response (matches middleware pattern).
- Pin server-side Supabase cookie name to one derived from
  NEXT_PUBLIC_SUPABASE_URL via cookieOptions.name. Without this, when
  SUPABASE_INTERNAL_URL and NEXT_PUBLIC_SUPABASE_URL have different
  hostnames (common in dev), browser-set sb-<ref>-auth-token doesn't
  match the cookie name the server reads, so getUser/signOut silently
  no-op.
- SignOutButton: hard-navigate via window.location to defeat RSC cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User 2 meses atrás
pai
commit
582a7fd

+ 27 - 7
src/app/api/auth/signout/route.ts

@@ -1,13 +1,33 @@
-import { NextResponse } from "next/server";
-import { getSupabaseServerClient } from "@/lib/supabase/server";
+import { createServerClient, type CookieOptions } from "@supabase/ssr";
+import { NextRequest, NextResponse } from "next/server";
+import { getSupabaseCookieName } from "@/lib/supabase/cookie-name";
 
-export async function POST() {
-  const supabase = await getSupabaseServerClient();
-  const { error } = await supabase.auth.signOut();
+export async function POST(request: NextRequest) {
+  const response = NextResponse.json({ ok: true });
+
+  const supabaseUrl = process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
+  const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
+  if (!supabaseUrl || !supabaseAnonKey) {
+    return NextResponse.json({ error: "Supabase not configured" }, { status: 500 });
+  }
 
+  const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
+    cookieOptions: { name: getSupabaseCookieName() },
+    cookies: {
+      getAll() {
+        return request.cookies.getAll();
+      },
+      setAll(cookiesToSet: Array<{ name: string; value: string; options: CookieOptions }>) {
+        cookiesToSet.forEach(({ name, value, options }) =>
+          response.cookies.set(name, value, options),
+        );
+      },
+    },
+  });
+
+  const { error } = await supabase.auth.signOut();
   if (error) {
     return NextResponse.json({ error: "Failed to sign out" }, { status: 500 });
   }
-
-  return NextResponse.json({ ok: true });
+  return response;
 }

+ 31 - 0
src/app/logout/route.ts

@@ -0,0 +1,31 @@
+import { createServerClient, type CookieOptions } from "@supabase/ssr";
+import { NextRequest, NextResponse } from "next/server";
+import { getSupabaseCookieName } from "@/lib/supabase/cookie-name";
+
+async function handle(request: NextRequest) {
+  const response = NextResponse.redirect(new URL("/", request.url));
+
+  const supabaseUrl = process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
+  const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
+  if (!supabaseUrl || !supabaseAnonKey) return response;
+
+  const supabase = createServerClient(supabaseUrl, supabaseAnonKey, {
+    cookieOptions: { name: getSupabaseCookieName() },
+    cookies: {
+      getAll() {
+        return request.cookies.getAll();
+      },
+      setAll(cookiesToSet: Array<{ name: string; value: string; options: CookieOptions }>) {
+        cookiesToSet.forEach(({ name, value, options }) =>
+          response.cookies.set(name, value, options),
+        );
+      },
+    },
+  });
+
+  await supabase.auth.signOut();
+  return response;
+}
+
+export const GET = handle;
+export const POST = handle;

+ 1 - 4
src/components/auth/sign-out-button.tsx

@@ -1,11 +1,9 @@
 "use client";
 
 import { useState } from "react";
-import { useRouter } from "next/navigation";
 import { useQueryClient } from "@tanstack/react-query";
 
 export function SignOutButton() {
-  const router = useRouter();
   const queryClient = useQueryClient();
   const [pending, setPending] = useState(false);
 
@@ -13,8 +11,7 @@ export function SignOutButton() {
     setPending(true);
     await fetch("/api/auth/signout", { method: "POST" });
     queryClient.clear();
-    router.push("/");
-    router.refresh();
+    window.location.assign("/");
   }
 
   return (

+ 10 - 0
src/lib/supabase/cookie-name.ts

@@ -0,0 +1,10 @@
+export function getSupabaseCookieName(): string {
+  const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
+  if (!url) return "sb-auth-token";
+  try {
+    const host = new URL(url).hostname;
+    return `sb-${host.split(".")[0]}-auth-token`;
+  } catch {
+    return "sb-auth-token";
+  }
+}

+ 2 - 0
src/lib/supabase/server.ts

@@ -1,6 +1,7 @@
 import { createServerClient, type CookieOptions } from "@supabase/ssr";
 import { cookies } from "next/headers";
 import type { Database } from "@/types/database";
+import { getSupabaseCookieName } from "./cookie-name";
 
 export async function getSupabaseServerClient() {
   const cookieStore = await cookies();
@@ -9,6 +10,7 @@ export async function getSupabaseServerClient() {
     process.env.SUPABASE_INTERNAL_URL || process.env.NEXT_PUBLIC_SUPABASE_URL!,
     process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
     {
+      cookieOptions: { name: getSupabaseCookieName() },
       cookies: {
         getAll() {
           return cookieStore.getAll();