72 lines
2 KiB
TypeScript
72 lines
2 KiB
TypeScript
import { SignJWT, jwtVerify, type JWTPayload } from "jose";
|
|
import type { NextRequest } from "next/server";
|
|
|
|
const SESSION_COOKIE = "admin_session";
|
|
const SESSION_TTL = 28_800; // 8 hours in seconds
|
|
|
|
function getJwtSecret(): Uint8Array {
|
|
const secret = process.env.ADMIN_JWT_SECRET;
|
|
if (!secret || secret.length < 32) {
|
|
throw new Error(
|
|
"ADMIN_JWT_SECRET env var must be set to a string of at least 32 characters",
|
|
);
|
|
}
|
|
return new TextEncoder().encode(secret);
|
|
}
|
|
|
|
export interface AdminSession extends JWTPayload {
|
|
ip: string;
|
|
}
|
|
|
|
/** Creates a signed JWT and returns it as the cookie value. */
|
|
export async function createSession(ip: string): Promise<string> {
|
|
const token = await new SignJWT({ ip } satisfies Partial<AdminSession>)
|
|
.setProtectedHeader({ alg: "HS256" })
|
|
.setIssuedAt()
|
|
.setExpirationTime(`${SESSION_TTL}s`)
|
|
.sign(getJwtSecret());
|
|
return token;
|
|
}
|
|
|
|
/**
|
|
* Verifies the session JWT from the request cookie.
|
|
* Works in both Edge and Node.js runtimes (uses Web Crypto).
|
|
*/
|
|
export async function getSession(
|
|
req: NextRequest,
|
|
): Promise<AdminSession | null> {
|
|
const token = req.cookies.get(SESSION_COOKIE)?.value;
|
|
if (!token) return null;
|
|
try {
|
|
const { payload } = await jwtVerify<AdminSession>(token, getJwtSecret());
|
|
return payload;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroys a session — with JWT sessions the cookie is cleared client-side.
|
|
* No server-side state to remove.
|
|
*/
|
|
export async function destroySession(_req: NextRequest): Promise<void> {
|
|
// JWT sessions are stateless — clearing the cookie in the response is sufficient.
|
|
}
|
|
|
|
export const SESSION_COOKIE_NAME = SESSION_COOKIE;
|
|
|
|
export function makeSessionCookie(token: string, secure: boolean): string {
|
|
const parts = [
|
|
`${SESSION_COOKIE}=${token}`,
|
|
"HttpOnly",
|
|
"SameSite=Strict",
|
|
`Max-Age=${SESSION_TTL}`,
|
|
"Path=/",
|
|
];
|
|
if (secure) parts.push("Secure");
|
|
return parts.join("; ");
|
|
}
|
|
|
|
export function clearSessionCookie(): string {
|
|
return `${SESSION_COOKIE}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/`;
|
|
}
|