From ce447d3a2c8fbb4e285ee947442c68163089ec6f Mon Sep 17 00:00:00 2001 From: Aarav Sharma Date: Wed, 19 Aug 2026 21:40:16 -0600 Subject: [PATCH] feat(auth): brand the NextAuth signout confirmation page NextAuth rendered its own unstyled confirmation page on GET /api/auth/signout because authOptions.pages only overrode signIn and error, so the signout flow looked off-brand even though the rest of the app uses kilo-design tokens and AuthPageLayout. Worse, every in-app signout entry point (sidebar dropdown, profile sign-out-browser-sessions dialog, device-auth flow) bypassed any confirmation by calling signOut({ callbackUrl }) from next-auth/react, which POSTs /api/auth/signout and follows the returned redirect directly. Add a /users/sign_out page that reuses AuthPageLayout, the existing Card and Button primitives, and the same revoke-web-session + signOut({ callbackUrl }) flow used elsewhere. The page reads callbackUrl from the query string, restricts it to same-origin paths, and falls back to /profile when absent. Point NextAuth pages.signOut at it, so any external or manual GET /api/auth/signout lands on the branded page, and route the in-app signout entry points through /users/sign_out?callbackUrl=... so they share the same confirmation UI. Update the dev 'nuke' action to target /users/sign_out?callbackUrl=/profile directly because NextAuth's pages.signOut redirect drops the original query string (see node_modules/next-auth/src/core/index.ts:199). --- .../(app)/components/SidebarUserFooter.tsx | 9 +-- .../src/app/device-auth/DeviceAuthClient.tsx | 11 +--- apps/web/src/app/users/sign_out/page.tsx | 66 +++++++++++++++++++ apps/web/src/components/dev/actions.ts | 2 +- apps/web/src/lib/user/server.ts | 1 + 5 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 apps/web/src/app/users/sign_out/page.tsx diff --git a/apps/web/src/app/(app)/components/SidebarUserFooter.tsx b/apps/web/src/app/(app)/components/SidebarUserFooter.tsx index 916b223423..254691b45d 100644 --- a/apps/web/src/app/(app)/components/SidebarUserFooter.tsx +++ b/apps/web/src/app/(app)/components/SidebarUserFooter.tsx @@ -11,7 +11,6 @@ import { } from '@/components/ui/dropdown-menu'; import { Avatar, AvatarImage, AvatarFallback } from '@radix-ui/react-avatar'; import { BookOpen, ChevronsUpDown, Download, FileDown, LogOut, UserCog } from 'lucide-react'; -import { signOut } from 'next-auth/react'; import { useRouter } from 'next/navigation'; type User = { @@ -37,12 +36,8 @@ function getUserInitials(name: string) { export default function SidebarUserFooter({ user, isLoading }: SidebarUserFooterProps) { const router = useRouter(); - const handleLogout = async () => { - try { - await fetch('/api/auth/revoke-web-session', { method: 'POST' }); - } finally { - await signOut({ callbackUrl: '/' }); - } + const handleLogout = () => { + router.push('/users/sign_out?callbackUrl=%2F'); }; return ( diff --git a/apps/web/src/app/device-auth/DeviceAuthClient.tsx b/apps/web/src/app/device-auth/DeviceAuthClient.tsx index 86240dfcc9..39e310a0e3 100644 --- a/apps/web/src/app/device-auth/DeviceAuthClient.tsx +++ b/apps/web/src/app/device-auth/DeviceAuthClient.tsx @@ -1,7 +1,6 @@ 'use client'; import { useState } from 'react'; -import { signOut } from 'next-auth/react'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; @@ -55,14 +54,10 @@ export function DeviceAuthClient({ code, viewerToken, isAppMode, user }: DeviceA const shellClassName = getDeviceAuthShellClassName(isAppMode); const outcomeHeaderClassName = getDeviceAuthOutcomeHeaderClassName(); - const handleSignOut = async () => { + const handleSignOut = () => { setIsSigningOut(true); - - try { - await fetch('/api/auth/revoke-web-session', { method: 'POST' }); - } finally { - await signOut({ callbackUrl: getDeviceAuthSignInUrl(code, { app: isAppMode }) }); - } + const callbackUrl = getDeviceAuthSignInUrl(code, { app: isAppMode }); + window.location.assign(`/users/sign_out?callbackUrl=${encodeURIComponent(callbackUrl)}`); }; const redirectToSignIn = () => { diff --git a/apps/web/src/app/users/sign_out/page.tsx b/apps/web/src/app/users/sign_out/page.tsx new file mode 100644 index 0000000000..a036b887f6 --- /dev/null +++ b/apps/web/src/app/users/sign_out/page.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useRouter, useSearchParams } from 'next/navigation'; +import { signOut } from 'next-auth/react'; +import { useState } from 'react'; + +import { AuthPageLayout } from '@/components/auth/AuthPageLayout'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from '@/components/ui/card'; + +function safeCallbackUrl(raw: string | null): string { + if (!raw) return '/profile'; + if (!raw.startsWith('/') || raw.startsWith('//')) return '/profile'; + return raw; +} + +export default function SignOutPage() { + const router = useRouter(); + const params = useSearchParams(); + const callbackUrl = safeCallbackUrl(params.get('callbackUrl')); + const [submitting, setSubmitting] = useState(false); + + const handleSignOut = async () => { + if (submitting) return; + setSubmitting(true); + try { + await fetch('/api/auth/revoke-web-session', { method: 'POST' }); + } finally { + await signOut({ callbackUrl }); + } + }; + + return ( + + + + Sign out of Kilo? + + You'll need to sign in again to access your account. + + + + + + + + + + ); +} \ No newline at end of file diff --git a/apps/web/src/components/dev/actions.ts b/apps/web/src/components/dev/actions.ts index 24fab1e94a..be68f5276d 100644 --- a/apps/web/src/components/dev/actions.ts +++ b/apps/web/src/components/dev/actions.ts @@ -33,5 +33,5 @@ export async function nuke() { throw new Error('Failed to nuke account. Please try again later.'); } - redirect('/api/auth/signout?callbackUrl=/profile'); + redirect('/users/sign_out?callbackUrl=/profile'); } diff --git a/apps/web/src/lib/user/server.ts b/apps/web/src/lib/user/server.ts index b0d5374211..b645685206 100644 --- a/apps/web/src/lib/user/server.ts +++ b/apps/web/src/lib/user/server.ts @@ -1058,6 +1058,7 @@ export const authOptions: NextAuthOptions = { pages: { signIn: '/users/sign_in', error: '/users/sign_in', + signOut: '/users/sign_out', }, debug: !!getEnvVariable('DEBUG_AUTH'), };