PraxaLing / lib /auth /session.ts
Reubencf's picture
Fix logout, OAuth state cleanup, HF_TOKEN diagnostic
0b4d29f
import { SignJWT, jwtVerify } from "jose";
import { cookies, headers } from "next/headers";
import type { NextResponse } from "next/server";
export const SESSION_COOKIE = "ll_session";
const SESSION_TTL_SECONDS = 60 * 60 * 24 * 7;
function secret() {
const s = process.env.AUTH_SECRET;
if (!s) throw new Error("AUTH_SECRET is not set");
return new TextEncoder().encode(s);
}
export type SessionPayload = {
hfId: string;
hfUsername: string;
email?: string;
avatarUrl?: string;
nativeLang?: string;
targetLang?: string;
targetLangs?: string[];
level?: string;
accessToken?: string;
streakCount?: number;
lastActiveDate?: string;
};
export async function signSession(payload: SessionPayload): Promise<string> {
return new SignJWT({ ...payload })
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime(`${SESSION_TTL_SECONDS}s`)
.sign(secret());
}
export async function verifySession(token: string): Promise<SessionPayload | null> {
try {
const { payload } = await jwtVerify<SessionPayload>(token, secret(), { algorithms: ["HS256"] });
if (!payload.hfId) return null;
return {
hfId: payload.hfId,
hfUsername: payload.hfUsername,
email: payload.email,
avatarUrl: payload.avatarUrl,
nativeLang: payload.nativeLang,
targetLang: payload.targetLang,
targetLangs: payload.targetLangs,
level: payload.level,
accessToken: payload.accessToken,
streakCount: payload.streakCount,
lastActiveDate: payload.lastActiveDate,
};
} catch {
return null;
}
}
export async function getSession(): Promise<SessionPayload | null> {
const authHeader = (await headers()).get("authorization");
if (authHeader?.toLowerCase().startsWith("bearer ")) {
const token = authHeader.slice(7).trim();
if (token) return verifySession(token);
}
const token = (await cookies()).get(SESSION_COOKIE)?.value;
if (!token) return null;
return verifySession(token);
}
export function sessionCookieOptions(maxAge: number) {
const isProd = process.env.NODE_ENV === "production";
return {
httpOnly: true,
secure: isProd,
sameSite: (isProd ? "none" : "lax") as "none" | "lax",
path: "/",
maxAge,
};
}
export function setSessionCookie(res: NextResponse, token: string) {
res.cookies.set(SESSION_COOKIE, token, sessionCookieOptions(SESSION_TTL_SECONDS));
}
export function clearSessionCookie(res: NextResponse) {
res.cookies.set(SESSION_COOKIE, "", sessionCookieOptions(0));
}
export const SESSION_MAX_AGE = SESSION_TTL_SECONDS;