File size: 2,588 Bytes
dfa877e
 
0b4d29f
dfa877e
 
 
 
 
 
 
 
 
 
 
a0a94bd
dfa877e
a0a94bd
 
 
 
16fde87
a0a94bd
 
16fde87
 
dfa877e
 
 
 
 
 
 
 
 
 
 
 
 
a0a94bd
 
 
 
 
 
 
 
16fde87
a0a94bd
 
16fde87
 
a0a94bd
dfa877e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0b4d29f
5aa0d89
0b4d29f
dfa877e
5aa0d89
0b4d29f
dfa877e
0b4d29f
 
dfa877e
 
0b4d29f
 
 
 
 
 
dfa877e
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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;