import type { SessionUser } from '@nexumi/shared'; import { type NextRequest, NextResponse } from 'next/server'; import { exchangeCodeForToken, fetchDiscordUser } from '@/lib/discord-oauth'; import { env } from '@/lib/env'; import { redis } from '@/lib/redis'; import { applySessionCookie, createSession } from '@/lib/session'; function oauthStateKey(state: string): string { return `webui:oauth:state:${state}`; } function loginRedirect(error: string): NextResponse { const url = new URL('/login', env.WEBUI_URL); url.searchParams.set('error', error); return NextResponse.redirect(url); } export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const code = searchParams.get('code'); const state = searchParams.get('state'); const oauthError = searchParams.get('error'); if (oauthError) { return loginRedirect('access_denied'); } if (!code || !state) { return loginRedirect('invalid_request'); } const stateKey = oauthStateKey(state); const stateExists = await redis.get(stateKey); if (!stateExists) { return loginRedirect('invalid_state'); } await redis.del(stateKey); let cookieValue: string; try { const token = await exchangeCodeForToken(code); const discordUser = await fetchDiscordUser(token.access_token); const user: SessionUser = { id: discordUser.id, username: discordUser.username, globalName: discordUser.global_name ?? null, avatar: discordUser.avatar ?? null }; cookieValue = await createSession({ user, accessToken: token.access_token, tokenExpiresAt: Date.now() + token.expires_in * 1000 }); } catch (error) { console.error('OAuth callback failed', error); return loginRedirect('oauth_failed'); } // Cookie must be set on the redirect response — cookies().set() alone is not // reliably attached when returning NextResponse.redirect() from a route handler. const response = NextResponse.redirect(new URL('/dashboard', env.WEBUI_URL)); applySessionCookie(response, cookieValue); return response; }