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>
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// RAPPORT-HOST — Betreiber-Bereich (/api/admin). Nur für is_admin-Konten.
|
||||
// Übersicht aller Kunden, Abos, Instanzen + Sperren/Reaktivieren.
|
||||
import { Router } from "express";
|
||||
import { one, query } from "../db.js";
|
||||
import { requireAdmin } from "../auth.js";
|
||||
import { getPlan } from "../plans.js";
|
||||
|
||||
export const adminRouter = Router();
|
||||
|
||||
adminRouter.use(requireAdmin);
|
||||
|
||||
// — Kennzahlen fürs Dashboard —
|
||||
adminRouter.get("/stats", async (_req, res) => {
|
||||
const accounts = await one("select count(*)::int n from accounts");
|
||||
const activeSubs = await one("select count(*)::int n from subscriptions where status = 'active'");
|
||||
const instances = await one("select count(*)::int n from instances");
|
||||
const activeInst = await one("select count(*)::int n from instances where status = 'active'");
|
||||
|
||||
// MRR (geschätzt): Summe der Plan-Preise aller aktiven Abos.
|
||||
const { rows: subs } = await query("select plan from subscriptions where status = 'active'");
|
||||
const mrr = subs.reduce((sum, s) => sum + (getPlan(s.plan)?.priceChf || 0), 0);
|
||||
|
||||
res.json({
|
||||
accounts: accounts.n,
|
||||
activeSubscriptions: activeSubs.n,
|
||||
instances: instances.n,
|
||||
activeInstances: activeInst.n,
|
||||
mrrChf: mrr,
|
||||
});
|
||||
});
|
||||
|
||||
// — Alle Kunden mit Abo + Instanzen —
|
||||
adminRouter.get("/accounts", async (_req, res) => {
|
||||
const { rows } = await query(`
|
||||
select a.id, a.email, a.company, a.is_admin, a.created_at,
|
||||
s.plan, s.status as sub_status, s.current_period_end,
|
||||
coalesce(i.cnt, 0)::int as instance_count
|
||||
from accounts a
|
||||
left join lateral (
|
||||
select plan, status, current_period_end
|
||||
from subscriptions where account_id = a.id
|
||||
order by created_at desc limit 1
|
||||
) s on true
|
||||
left join (
|
||||
select account_id, count(*) cnt from instances group by account_id
|
||||
) i on i.account_id = a.id
|
||||
order by a.created_at desc
|
||||
`);
|
||||
res.json({ accounts: rows });
|
||||
});
|
||||
|
||||
// — Instanzen eines Kontos —
|
||||
adminRouter.get("/accounts/:id/instances", async (req, res) => {
|
||||
const { rows } = await query(
|
||||
"select id, studio_slug, label, instance_url, status, created_at from instances where account_id = $1 order by created_at",
|
||||
[req.params.id]
|
||||
);
|
||||
res.json({ instances: rows });
|
||||
});
|
||||
|
||||
// — Instanz sperren / reaktivieren —
|
||||
adminRouter.post("/instances/:id/:action", async (req, res) => {
|
||||
const { id, action } = req.params;
|
||||
const map = { suspend: "suspended", reactivate: "active" };
|
||||
const status = map[action];
|
||||
if (!status) return res.status(400).json({ error: "Unbekannte Aktion." });
|
||||
const row = await one("update instances set status = $1 where id = $2 returning id, status", [status, id]);
|
||||
if (!row) return res.status(404).json({ error: "Instanz nicht gefunden." });
|
||||
res.json({ instance: row });
|
||||
});
|
||||
Reference in New Issue
Block a user