v2: Session-Login & Rollen, Premium-Admin, Visual-Block-Builder, KI-/MCP-API

- Auth-Umbau: Session-Login (signiertes HMAC-Cookie, scrypt-Hashing) statt Basic-Auth;
  users-/audit-Tabellen, Initial-Owner aus ENV, Rate-Limit, konfigurierbarer ADMIN_PATH
  (Middleware-Rewrite), Rollen-Gate (owner/redaktion/versand), Nutzerverwaltung, Audit-Log,
  Login/Logout/Konto-Seiten.
- Premium-Pass: Command-Palette (Cmd-K), Toasts, Account-Menue, aufgewertetes Dashboard
  (KPI-Trend+Sparkline, Aktivitaets-Feed, Schnellaktionen), schoene Empty-States.
- Block-Builder: pages.blocks, Vollbild-Editor (Liste/Live-Vorschau/Settings, Desktop/Mobil),
  10 Block-Typen, Storefront-BlockRenderer auf /seite/[slug], Save-Endpoint.
- KI-Editierbarkeit: token-gesicherte /api/admin/* (CRUD), Manifest /api/admin + /ai-admin.txt,
  MCP-Server unter mcp/ (14 Tools).
- Docs: README + .env.example + mcp/README aktualisiert.
This commit is contained in:
2026-06-17 12:46:31 +00:00
parent 3c48b69880
commit aec179db36
41 changed files with 9525 additions and 143 deletions
+6 -4
View File
@@ -1,21 +1,23 @@
---
import Admin from '../../../layouts/Admin.astro';
import { getOrderById, updateOrderStatus, formatPrice } from '../../../lib/store.js';
import { adminBase, currentUser } from '../../../lib/auth.js';
const base = adminBase();
import { getOrderById, updateOrderStatus, formatPrice, recordAudit } from '../../../lib/store.js';
const { id } = Astro.params;
let flash = '';
if (Astro.request.method === 'POST') {
const form = await Astro.request.formData();
const status = form.get('status');
if (status) { updateOrderStatus(id, String(status)); flash = 'Status aktualisiert.'; }
if (status) { updateOrderStatus(id, String(status)); recordAudit({ user: currentUser(Astro.request)?.email, action: 'update', entity: 'order', entity_id: String(id) }); flash = 'Status aktualisiert.'; }
}
const order = getOrderById(id);
if (!order) return Astro.redirect('/admin/bestellungen');
if (!order) return Astro.redirect(base + '/bestellungen');
const statusMap = { fulfilled: ['green', 'Erfüllt'], pending: ['amber', 'Offen'], cancelled: ['gray', 'Storniert'], refunded: ['red', 'Erstattet'] };
const fmtDate = (s) => new Date(s).toLocaleString('de-DE', { day: '2-digit', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit' });
const statuses = [['pending', 'Offen'], ['fulfilled', 'Erfüllt'], ['cancelled', 'Storniert'], ['refunded', 'Erstattet']];
---
<Admin title={`Bestellung ${order.number}`} active="bestellungen" crumbs={[{ label: 'Bestellungen', href: '/admin/bestellungen' }, { label: order.number }]}>
<Admin title={`Bestellung ${order.number}`} active="bestellungen" crumbs={[{ label: 'Bestellungen', href: base + '/bestellungen' }, { label: order.number }]}>
<div class="s-stack">
{flash && <div class="s-flash">✓ {flash}</div>}
<div class="s-two-col">
+4 -2
View File
@@ -1,5 +1,7 @@
---
import Admin from '../../../layouts/Admin.astro';
import { adminBase } from '../../../lib/auth.js';
const base = adminBase();
import { listOrders, formatPrice } from '../../../lib/store.js';
const orders = listOrders();
const statusMap = { fulfilled: ['green', 'Erfüllt'], pending: ['amber', 'Offen'], cancelled: ['gray', 'Storniert'], refunded: ['red', 'Erstattet'] };
@@ -11,9 +13,9 @@ const fmtDate = (s) => new Date(s).toLocaleDateString('de-DE', { day: '2-digit',
<table class="s-table">
<thead><tr><th>Bestellung</th><th>Datum</th><th>Kunde</th><th>Artikel</th><th>Status</th><th class="num">Betrag</th></tr></thead>
<tbody>
{orders.length === 0 ? (<tr><td colspan="6" class="s-empty">Noch keine Bestellungen</td></tr>) :
{orders.length === 0 ? (<tr><td colspan="6"><div class="s-emptystate"><div class="es-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4H6Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg></div><h3>Noch keine Bestellungen</h3><p>Sobald Kund:innen im Shop kaufen, erscheinen die Bestellungen hier.</p><a class="s-btn" href="/" target="_blank">Shop ansehen ↗</a></div></td></tr>) :
orders.map((o) => (
<tr class="clk" onclick={`location.href='/admin/bestellungen/${o.id}'`}>
<tr class="clk" onclick={`location.href='${base}/bestellungen/${o.id}'`}>
<td><b>{o.number}</b></td>
<td class="s-muted">{fmtDate(o.created_at)}</td>
<td>{o.customer_name || '—'}<div class="s-muted" style="font-size:12px">{o.email}</div></td>