| 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; |
|
|