makearmy-app/lib/auth-cookies.ts

102 lines
2.8 KiB
TypeScript

// app/lib/auth-cookies.ts
import { NextResponse } from "next/server";
export type TokenBundle = {
access_token: string;
refresh_token?: string;
/** seconds until expiration (Directus style) */
expires?: number;
};
export type PublicUser = {
id: string;
email: string;
username: string;
};
const ACCESS_COOKIE = "ma_at";
const REFRESH_COOKIE = "ma_rt";
const USER_COOKIE = "ma_user";
/** Derive cookie maxAge (in seconds) for access token */
function accessMaxAgeSec(expires?: number) {
// If Directus gave us seconds-until-expiration, use that (clamped)
if (typeof expires === "number" && Number.isFinite(expires)) {
return Math.max(60, Math.min(expires, 60 * 60 * 24)); // 1 min .. 1 day
}
// Fallback: 1 hour
return 60 * 60;
}
/** Refresh token lifetime: default ~30 days if present */
function refreshMaxAgeSec() {
return 60 * 60 * 24 * 30;
}
/** Shared secure cookie options (override per cookie when needed) */
function baseOpts(maxAge: number) {
return {
httpOnly: true as const,
sameSite: "lax" as const,
secure: true,
path: "/",
maxAge,
};
}
/**
* Set auth cookies on the provided response.
* Returns the SAME response instance with cookies set (typed generically).
*/
export function setAuthCookies<T>(
res: NextResponse<T>,
tokens: TokenBundle,
user?: PublicUser
): NextResponse<T> {
// Access token (httpOnly)
const atAge = accessMaxAgeSec(tokens.expires);
res.cookies.set(ACCESS_COOKIE, tokens.access_token, baseOpts(atAge));
// Refresh token (httpOnly) if present
if (tokens.refresh_token) {
res.cookies.set(REFRESH_COOKIE, tokens.refresh_token, baseOpts(refreshMaxAgeSec()));
}
// Small readable user stub (NOT httpOnly) so client can reflect UI state if desired
if (user) {
const safeStub = JSON.stringify({
id: user.id,
username: user.username,
email: user.email,
});
res.cookies.set(USER_COOKIE, safeStub, {
...baseOpts(atAge),
httpOnly: false, // readable on client
});
}
return res;
}
/** Clear all auth cookies (returns the SAME response instance) */
export function clearAuthCookies<T>(res: NextResponse<T>): NextResponse<T> {
const opts = {
httpOnly: true as const,
sameSite: "lax" as const,
secure: true,
path: "/",
maxAge: 0,
};
res.cookies.set(ACCESS_COOKIE, "", opts);
res.cookies.set(REFRESH_COOKIE, "", opts);
// Also clear public user stub
res.cookies.set(USER_COOKIE, "", { ...opts, httpOnly: false });
return res;
}
/** (Optional) Simple helpers if you ever want the names elsewhere */
export const AUTH_COOKIE_KEYS = {
access: ACCESS_COOKIE,
refresh: REFRESH_COOKIE,
user: USER_COOKIE,
};