Refactor session management and enhance OAuth2 callback handling
- Updated session management functions to include cookie handling for session creation and destruction. - Introduced applySessionCookie and clearSessionCookie functions for better cookie management in responses. - Enhanced error logging in the OAuth2 callback to improve debugging. - Revised PHASE-TRACKING.md to reflect changes in session handling and OAuth2 integration.
This commit is contained in:
@@ -3,7 +3,7 @@ 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 { createSession } from '@/lib/session';
|
||||
import { applySessionCookie, createSession } from '@/lib/session';
|
||||
|
||||
function oauthStateKey(state: string): string {
|
||||
return `webui:oauth:state:${state}`;
|
||||
@@ -36,6 +36,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
await redis.del(stateKey);
|
||||
|
||||
let cookieValue: string;
|
||||
try {
|
||||
const token = await exchangeCodeForToken(code);
|
||||
const discordUser = await fetchDiscordUser(token.access_token);
|
||||
@@ -47,14 +48,19 @@ export async function GET(request: NextRequest) {
|
||||
avatar: discordUser.avatar ?? null
|
||||
};
|
||||
|
||||
await createSession({
|
||||
cookieValue = await createSession({
|
||||
user,
|
||||
accessToken: token.access_token,
|
||||
tokenExpiresAt: Date.now() + token.expires_in * 1000
|
||||
});
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('OAuth callback failed', error);
|
||||
return loginRedirect('oauth_failed');
|
||||
}
|
||||
|
||||
return NextResponse.redirect(new URL('/dashboard', env.WEBUI_URL));
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { destroySession } from '@/lib/session';
|
||||
import { clearSessionCookie, destroySession } from '@/lib/session';
|
||||
|
||||
export async function POST() {
|
||||
await destroySession();
|
||||
return NextResponse.json({ ok: true });
|
||||
const response = NextResponse.json({ ok: true });
|
||||
clearSessionCookie(response);
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createHmac, randomBytes, timingSafeEqual } from 'crypto';
|
||||
import { cookies } from 'next/headers';
|
||||
import type { NextResponse } from 'next/server';
|
||||
import { z } from 'zod';
|
||||
import { SessionUserSchema } from '@nexumi/shared';
|
||||
import { env } from './env';
|
||||
@@ -52,20 +53,41 @@ function sessionRedisKey(sessionId: string): string {
|
||||
return `${SESSION_KEY_PREFIX}${sessionId}`;
|
||||
}
|
||||
|
||||
export async function createSession(payload: SessionPayload): Promise<void> {
|
||||
const sessionId = randomBytes(32).toString('hex');
|
||||
await redis.set(sessionRedisKey(sessionId), JSON.stringify(payload), 'EX', SESSION_TTL_SECONDS);
|
||||
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set(SESSION_COOKIE_NAME, buildCookieValue(sessionId), {
|
||||
/** Secure cookies only when the public WebUI URL is HTTPS (HTTP IP deploys must not use Secure). */
|
||||
export function sessionCookieOptions(): {
|
||||
httpOnly: boolean;
|
||||
sameSite: 'lax';
|
||||
secure: boolean;
|
||||
path: string;
|
||||
maxAge: number;
|
||||
} {
|
||||
return {
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
secure: env.NODE_ENV === 'production',
|
||||
secure: env.WEBUI_URL.startsWith('https://'),
|
||||
path: '/',
|
||||
maxAge: SESSION_TTL_SECONDS
|
||||
};
|
||||
}
|
||||
|
||||
export function applySessionCookie(response: NextResponse, cookieValue: string): void {
|
||||
response.cookies.set(SESSION_COOKIE_NAME, cookieValue, sessionCookieOptions());
|
||||
}
|
||||
|
||||
export function clearSessionCookie(response: NextResponse): void {
|
||||
response.cookies.set(SESSION_COOKIE_NAME, '', {
|
||||
...sessionCookieOptions(),
|
||||
maxAge: 0
|
||||
});
|
||||
}
|
||||
|
||||
/** Persists the session in Redis and returns the signed cookie value to attach to a response. */
|
||||
export async function createSession(payload: SessionPayload): Promise<string> {
|
||||
const sessionId = randomBytes(32).toString('hex');
|
||||
await redis.set(sessionRedisKey(sessionId), JSON.stringify(payload), 'EX', SESSION_TTL_SECONDS);
|
||||
return buildCookieValue(sessionId);
|
||||
}
|
||||
|
||||
export async function getSession(): Promise<SessionPayload | null> {
|
||||
const cookieStore = await cookies();
|
||||
const raw = cookieStore.get(SESSION_COOKIE_NAME)?.value;
|
||||
@@ -93,7 +115,6 @@ export async function destroySession(): Promise<void> {
|
||||
await redis.del(sessionRedisKey(sessionId));
|
||||
}
|
||||
}
|
||||
cookieStore.delete(SESSION_COOKIE_NAME);
|
||||
}
|
||||
|
||||
export async function requireSession(): Promise<SessionPayload> {
|
||||
|
||||
Reference in New Issue
Block a user