// hd-commerce — Session-Auth (stateless signiertes Cookie), Rollen-Gate, Rate-Limit. import { createHmac, timingSafeEqual } from 'node:crypto'; import { getUserById } from './store.js'; const SECRET = process.env.SESSION_SECRET || 'hd-commerce-dev-secret-change-me'; export const COOKIE_NAME = 'hdc_session'; // --- konfigurierbarer Admin-Pfad --- function rawAdminPath() { let p = (process.env.ADMIN_PATH || 'admin').trim().replace(/^\/+|\/+$/g, ''); if (!p) p = 'admin'; return p; } export const adminBase = () => '/' + rawAdminPath(); // z.B. "/login" oder "/admin" export const adminPathSegment = () => rawAdminPath(); // "login" export const isCustomAdminPath = () => rawAdminPath() !== 'admin'; // Hilfsfunktion für Links in Astro-Seiten: export const ab = (suffix = '') => adminBase() + (suffix || ''); // --- Cookie-Signatur --- function b64url(buf) { return Buffer.from(buf).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); } function b64urlDecode(str) { return Buffer.from(str.replace(/-/g, '+').replace(/_/g, '/'), 'base64'); } export function signSession(uid, maxAgeSeconds) { const exp = Math.floor(Date.now() / 1000) + (maxAgeSeconds || 60 * 60 * 12); const payload = b64url(JSON.stringify({ uid: Number(uid), exp })); const sig = b64url(createHmac('sha256', SECRET).update(payload).digest()); return payload + '.' + sig; } export function verifySession(token) { if (!token || typeof token !== 'string' || !token.includes('.')) return null; const [payload, sig] = token.split('.'); if (!payload || !sig) return null; const expected = b64url(createHmac('sha256', SECRET).update(payload).digest()); try { const a = Buffer.from(sig), b = Buffer.from(expected); if (a.length !== b.length || !timingSafeEqual(a, b)) return null; } catch { return null; } let data; try { data = JSON.parse(b64urlDecode(payload).toString('utf8')); } catch { return null; } if (!data || !data.uid || !data.exp) return null; if (data.exp < Math.floor(Date.now() / 1000)) return null; return data; } export function buildCookie(token, remember) { const parts = [`${COOKIE_NAME}=${token}`, 'Path=/', 'HttpOnly', 'SameSite=Lax']; if (remember) parts.push('Max-Age=' + (60 * 60 * 24 * 30)); return parts.join('; '); } export function clearCookie() { return `${COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`; } export function parseCookies(request) { const h = request.headers.get('cookie') || ''; const out = {}; h.split(';').forEach(p => { const i = p.indexOf('='); if (i > -1) out[p.slice(0, i).trim()] = decodeURIComponent(p.slice(i + 1).trim()); }); return out; } export function currentUser(request) { const token = parseCookies(request)[COOKIE_NAME]; const sess = verifySession(token); if (!sess) return null; const u = getUserById(sess.uid); if (!u || !u.active) return null; return u; } // --- Rollen-Gate --- // owner: alles · redaktion: Produkte/Inhalte/Marketing · versand: nur Bestellungen const ROLE_SECTIONS = { owner: ['dashboard', 'bestellungen', 'produkte', 'kunden', 'analytics', 'marketing', 'rabatte', 'inhalte', 'versandzonen', 'einstellungen', 'nutzer', 'audit'], redaktion: ['dashboard', 'produkte', 'inhalte', 'marketing', 'rabatte', 'analytics', 'versandzonen'], versand: ['bestellungen'], }; export function canAccess(role, section) { const allowed = ROLE_SECTIONS[role] || ROLE_SECTIONS.redaktion; return allowed.includes(section); } export function allowedSections(role) { return ROLE_SECTIONS[role] || ROLE_SECTIONS.redaktion; } export function landingFor(role) { if (role === 'versand') return adminBase() + '/bestellungen'; return adminBase(); } // --- Login-Rate-Limit (In-Memory) --- const attempts = new Map(); // ip -> { count, until } export function rateLimited(ip) { const r = attempts.get(ip); if (r && r.until && Date.now() < r.until) return true; return false; } export function registerFail(ip) { const r = attempts.get(ip) || { count: 0, until: 0 }; r.count += 1; if (r.count >= 5) { r.until = Date.now() + 60 * 1000; r.count = 0; } attempts.set(ip, r); } export function clearFails(ip) { attempts.delete(ip); } export function clientIp(request) { return (request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || 'local').split(',')[0].trim(); }