Files
hd-commerce/src/lib/auth.js
T
till e5514dd5da v2.2: Verkaufsfertig-Fundament — Mollie/Payment-Abstraktion, MwSt/Grundpreis (PAngV), Versandzonen, Bestellmails (Listmonk/SMTP/Log), Feature-Flags
- payments.js: einheitliche createPayment/Webhook-Schnittstelle (Mollie Default, Stripe, Demo); Auto-Provider-Wahl; Mollie-REST + /api/payments/webhook (idempotent); Fake-Key => sauberer Demo-Fallback
- mailer.js: sendMail via Listmonk-Tx / SMTP (nodemailer) / Log-Fallback (email_log); gebrandete Bestellbestaetigung bei paid
- DACH: products.mwst + base_amount/base_unit/base_price_per (Grundpreis); Storefront/Warenkorb/Checkout/Erfolg/Admin mit MwSt-Ausweis + Versand-Transparenz; tax_cents/shipping_cents/country an Orders
- shipping_zones-Tabelle + CRUD + shippingFor(); Admin 'Versand'; serverseitige Versandberechnung in /api/checkout + /api/shipping-quote (Laenderwahl live)
- Feature-Flags (feature_*) + feature()-Helper; Admin Module-Toggles; Newsletter-Gating (Popup/Subscribe)
- Admin-API/Manifest/ai-admin.txt um shipping_zones erweitert; MCP list/upsert/delete_shipping; README/.env.example ergaenzt; Version 2.2.0
2026-06-17 16:37:10 +00:00

112 lines
4.3 KiB
JavaScript

// 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();
}