feat(account): Profil-Felder, Instanz-Liste, Profil-Update + Passwort ändern
- 0002_account_profile.sql: company/contact_name/adresse/phone + instances.label - GET /account/me: alle Profilfelder + instances[] (Multi-Instanz vorbereitet), instance einzeln bleibt rückwärtskompatibel - PATCH /account/me: Profil aktualisieren (Whitelist) - POST /account/password: Passwort ändern (prüft aktuelles PW → 403 sonst) E2E verifiziert: Profil speichern, PW-Wechsel (falsch=403/richtig=200), instances[]=1 nach Checkout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
-- RAPPORT-HOST — Konto-Profilfelder (für "Mein Konto" + QR-Rechnung später).
|
||||||
|
-- Idempotent via IF NOT EXISTS, damit der Migrations-Runner mehrfach laufen kann.
|
||||||
|
|
||||||
|
alter table accounts add column if not exists company text;
|
||||||
|
alter table accounts add column if not exists contact_name text;
|
||||||
|
alter table accounts add column if not exists street text;
|
||||||
|
alter table accounts add column if not exists zip text;
|
||||||
|
alter table accounts add column if not exists city text;
|
||||||
|
alter table accounts add column if not exists country text default 'CH';
|
||||||
|
alter table accounts add column if not exists phone text;
|
||||||
|
alter table accounts add column if not exists updated_at timestamptz not null default now();
|
||||||
|
|
||||||
|
-- Optionaler Anzeigename pro Instanz (Kunde benennt seine Instanzen selbst),
|
||||||
|
-- damit eine Instanz-Liste im Konto lesbar ist (statt nur slug).
|
||||||
|
alter table instances add column if not exists label text;
|
||||||
@@ -1,26 +1,78 @@
|
|||||||
// Kunden-Dashboard-Daten: Konto + aktuelles Abo + bereitgestellte Instanz.
|
// Kunden-Konto: Profil, Abo, Instanzen (Liste), Profil-Update, Passwort ändern.
|
||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import { one } from "../db.js";
|
import { one, query } from "../db.js";
|
||||||
import { requireAuth } from "../auth.js";
|
import { requireAuth, hashPassword, verifyPassword } from "../auth.js";
|
||||||
import { publicPlans } from "../plans.js";
|
import { publicPlans } from "../plans.js";
|
||||||
|
|
||||||
export const accountRouter = Router();
|
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) => {
|
accountRouter.get("/me", requireAuth, async (req, res) => {
|
||||||
|
const account = await one(
|
||||||
|
`select id, email, company, contact_name, street, zip, city, country, phone, 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(
|
const subscription = await one(
|
||||||
`select plan, status, current_period_end, stripe_subscription_id
|
`select plan, status, current_period_end, stripe_subscription_id
|
||||||
from subscriptions where account_id = $1 order by created_at desc limit 1`,
|
from subscriptions where account_id = $1 order by created_at desc limit 1`,
|
||||||
[req.account.id]
|
[req.account.id]
|
||||||
);
|
);
|
||||||
const instance = await one(
|
const { rows: instances } = await query(
|
||||||
`select studio_slug, instance_url, status, created_at
|
`select id, studio_slug, label, instance_url, status, created_at
|
||||||
from instances where account_id = $1 order by created_at desc limit 1`,
|
from instances where account_id = $1 order by created_at asc`,
|
||||||
[req.account.id]
|
[req.account.id]
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
account: req.account,
|
account,
|
||||||
subscription: subscription || null,
|
subscription: subscription || null,
|
||||||
instance: instance || null,
|
instances,
|
||||||
|
// Rückwärtskompatibel: erste Instanz auch einzeln (altes Frontend).
|
||||||
|
instance: instances[0] || null,
|
||||||
plans: publicPlans(),
|
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 });
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user