Refactor content structure and add basic authentication utilities
- Moved event and gallery data to JSON files for cleaner content management. - Added session management utilities with CSRF protection. - Integrated OAuth-based login and logout APIs. - Updated dependencies, including Astro and introduced dotenv-cli. - Enhanced package.json with local environment support.
This commit is contained in:
87
src/lib/session.ts
Normal file
87
src/lib/session.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { env } from './env';
|
||||
|
||||
const SESSION_COOKIE = 'gp_session';
|
||||
const STATE_COOKIE = 'oauth_state';
|
||||
const CSRF_COOKIE = 'gp_csrf';
|
||||
|
||||
export type SessionData = {
|
||||
user?: {
|
||||
id: number;
|
||||
username: string;
|
||||
email?: string;
|
||||
displayName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
function hmac(value: string) {
|
||||
if (!env.SESSION_SECRET) throw new Error('SESSION_SECRET missing');
|
||||
return crypto.createHmac('sha256', env.SESSION_SECRET).update(value).digest('hex');
|
||||
}
|
||||
|
||||
export function encodeSession(data: SessionData): string {
|
||||
const json = JSON.stringify(data);
|
||||
const b64 = Buffer.from(json, 'utf-8').toString('base64url');
|
||||
const sig = hmac(b64);
|
||||
return `${b64}.${sig}`;
|
||||
}
|
||||
|
||||
export function decodeSession(token?: string | null): SessionData | null {
|
||||
if (!token) return null;
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 2) return null;
|
||||
const [b64, sig] = parts;
|
||||
const expected = hmac(b64);
|
||||
// timing-safe compare
|
||||
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return null;
|
||||
try {
|
||||
return JSON.parse(Buffer.from(b64, 'base64url').toString('utf-8')) as SessionData;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function cookieAttrs({ httpOnly = true, maxAge, path = '/', sameSite = 'Lax', secure }: { httpOnly?: boolean; maxAge?: number; path?: string; sameSite?: 'Lax'|'Strict'|'None'; secure?: boolean } = {}) {
|
||||
const attrs = [`Path=${path}`, `SameSite=${sameSite}`];
|
||||
if (httpOnly) attrs.push('HttpOnly');
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
const useSecure = secure ?? isProd;
|
||||
if (useSecure) attrs.push('Secure');
|
||||
if (typeof maxAge === 'number') attrs.push(`Max-Age=${maxAge}`);
|
||||
return attrs.join('; ');
|
||||
}
|
||||
|
||||
export function setSessionCookie(data: SessionData, maxAgeDays = 7): string {
|
||||
const token = encodeSession(data);
|
||||
const maxAge = maxAgeDays * 24 * 60 * 60;
|
||||
return `${SESSION_COOKIE}=${token}; ${cookieAttrs({ maxAge })}`;
|
||||
}
|
||||
|
||||
export function clearSessionCookie(): string {
|
||||
return `${SESSION_COOKIE}=; ${cookieAttrs({})}; Max-Age=0`;
|
||||
}
|
||||
|
||||
export function getSessionFromRequest(req: Request): SessionData | null {
|
||||
const cookie = parseCookies(req.headers.get('cookie'))[SESSION_COOKIE];
|
||||
return decodeSession(cookie);
|
||||
}
|
||||
|
||||
export function setTempCookie(name: string, value: string, maxAgeSeconds = 600): string {
|
||||
return `${name}=${value}; ${cookieAttrs({ maxAge: maxAgeSeconds })}`;
|
||||
}
|
||||
|
||||
export function clearCookie(name: string): string {
|
||||
return `${name}=; ${cookieAttrs({})}; Max-Age=0`;
|
||||
}
|
||||
|
||||
export function parseCookies(header: string | null | undefined): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
if (!header) return out;
|
||||
for (const part of header.split(';')) {
|
||||
const [k, ...rest] = part.trim().split('=');
|
||||
out[k] = decodeURIComponent(rest.join('='));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export { SESSION_COOKIE, STATE_COOKIE, CSRF_COOKIE };
|
||||
Reference in New Issue
Block a user