/**
* Admin Session Management
*
* KV Storage Pattern:
* - admin_session:{id} -> AdminSession JSON
*/
import type { AdminSession } from '../types';
const SESSION_COOKIE_NAME = '__Host-admin_session';
const SESSION_PREFIX = 'admin_session:';
const SESSION_TTL_SECONDS = 7 * 24 * 60 * 60; // 7 days
/**
* Create a new admin session
*/
export async function createAdminSession(
user: { email: string; name: string; picture?: string },
kv: KVNamespace
): Promise<{ sessionId: string; cookie: string }> {
const sessionId = crypto.randomUUID();
const expiresAt = Date.now() + SESSION_TTL_SECONDS * 1000;
const session: AdminSession = {
id: sessionId,
email: user.email,
name: user.name,
picture: user.picture,
expiresAt,
};
await kv.put(`${SESSION_PREFIX}${sessionId}`, JSON.stringify(session), {
expirationTtl: SESSION_TTL_SECONDS,
});
// SameSite=Lax is required for OAuth flows - Strict blocks cookies after cross-site redirects
const cookie = `${SESSION_COOKIE_NAME}=${sessionId}; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=${SESSION_TTL_SECONDS}`;
return { sessionId, cookie };
}
/**
* Get admin session by ID
*/
export async function getAdminSessionById(
sessionId: string,
kv: KVNamespace
): Promise<AdminSession | null> {
const data = await kv.get(`${SESSION_PREFIX}${sessionId}`);
if (!data) return null;
let session: AdminSession;
try {
session = JSON.parse(data) as AdminSession;
} catch {
// Corrupted session data - delete and return null
await kv.delete(`${SESSION_PREFIX}${sessionId}`);
return null;
}
// Check if expired (belt and suspenders with KV TTL)
if (session.expiresAt < Date.now()) {
await kv.delete(`${SESSION_PREFIX}${sessionId}`);
return null;
}
return session;
}
/**
* Delete admin session
*/
export async function deleteAdminSession(
sessionId: string,
kv: KVNamespace
): Promise<string> {
await kv.delete(`${SESSION_PREFIX}${sessionId}`);
// Return cookie that clears the session
return `${SESSION_COOKIE_NAME}=; HttpOnly; Secure; Path=/; SameSite=Strict; Max-Age=0`;
}
/**
* Extract session ID from request cookies
*/
function getSessionIdFromCookie(request: Request): string | null {
const cookieHeader = request.headers.get('Cookie');
if (!cookieHeader) return null;
const cookies = cookieHeader.split(';').map((c) => c.trim());
const sessionCookie = cookies.find((c) =>
c.startsWith(`${SESSION_COOKIE_NAME}=`)
);
if (!sessionCookie) return null;
return sessionCookie.substring(SESSION_COOKIE_NAME.length + 1);
}
/**
* Get admin session from request
*/
export async function getAdminSession(
request: Request,
kv: KVNamespace
): Promise<AdminSession | null> {
const sessionId = getSessionIdFromCookie(request);
if (!sessionId) return null;
return getAdminSessionById(sessionId, kv);
}