Files
RAPPORT-HOST/server/routes/account.js
T
karim 6a2393301d feat(admin): Betreiber-Panel (/api/admin) mit is_admin-Flag
- 0003_admin.sql: accounts.is_admin
- auth.js: ensureAdminFlag (Konto = ADMIN_EMAIL wird auto-promoted),
  is_admin im JWT, requireAdmin-Middleware (prüft DB autoritativ)
- routes/admin.js: GET /stats (Kunden/Abos/Instanzen/MRR), GET /accounts,
  GET /accounts/:id/instances, POST /instances/:id/{suspend,reactivate}
- register/login + /account/me liefern is_admin
- ADMIN_EMAIL in .env.example

E2E: Admin-Promotion, Kunde→403, Stats (2 Kunden/MRR 49), Kundenliste.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 00:04:19 +02:00

79 lines
3.3 KiB
JavaScript

// Kunden-Konto: Profil, Abo, Instanzen (Liste), Profil-Update, Passwort ändern.
import { Router } from "express";
import { one, query } from "../db.js";
import { requireAuth, hashPassword, verifyPassword } from "../auth.js";
import { publicPlans } from "../plans.js";
export const accountRouter = Router();
// Profil-Spalten, die der Kunde selbst bearbeiten darf (Whitelist).
const PROFILE_FIELDS = ["company", "contact_name", "street", "zip", "city", "country", "phone"];
// ── Konto-Übersicht: Konto + Profil + aktuelles Abo + alle Instanzen ─────────
accountRouter.get("/me", requireAuth, async (req, res) => {
const account = await one(
`select id, email, company, contact_name, street, zip, city, country, phone, is_admin, created_at
from accounts where id = $1`,
[req.account.id]
);
if (!account) return res.status(404).json({ error: "Konto nicht gefunden." });
const subscription = await one(
`select plan, status, current_period_end, stripe_subscription_id
from subscriptions where account_id = $1 order by created_at desc limit 1`,
[req.account.id]
);
const { rows: instances } = await query(
`select id, studio_slug, label, instance_url, status, created_at
from instances where account_id = $1 order by created_at asc`,
[req.account.id]
);
res.json({
account,
subscription: subscription || null,
instances,
// Rückwärtskompatibel: erste Instanz auch einzeln (altes Frontend).
instance: instances[0] || null,
plans: publicPlans(),
});
});
// ── Profil aktualisieren ─────────────────────────────────────────────────────
accountRouter.patch("/me", requireAuth, async (req, res) => {
const updates = [];
const values = [];
for (const f of PROFILE_FIELDS) {
if (req.body[f] !== undefined) {
values.push(req.body[f] === "" ? null : req.body[f]);
updates.push(`${f} = $${values.length}`);
}
}
if (!updates.length) return res.status(400).json({ error: "Keine Felder zum Aktualisieren." });
values.push(req.account.id);
const account = await one(
`update accounts set ${updates.join(", ")}, updated_at = now()
where id = $${values.length}
returning id, email, company, contact_name, street, zip, city, country, phone`,
values
);
res.json({ account });
});
// ── Passwort ändern ──────────────────────────────────────────────────────────
accountRouter.post("/password", requireAuth, async (req, res) => {
const { currentPassword, newPassword } = req.body || {};
if (!newPassword || newPassword.length < 8) {
return res.status(400).json({ error: "Neues Passwort min. 8 Zeichen." });
}
const row = await one("select password_hash from accounts where id = $1", [req.account.id]);
if (!row || !(await verifyPassword(currentPassword || "", row.password_hash))) {
return res.status(403).json({ error: "Aktuelles Passwort falsch." });
}
await query("update accounts set password_hash = $1, updated_at = now() where id = $2", [
await hashPassword(newPassword),
req.account.id,
]);
res.json({ ok: true });
});