102 lines
2.8 KiB
TypeScript
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,
|
|
};
|