feat: Admin-Bereich im Kundenportal (Ticketsystem-Login, Projektsuche, Kundenverwaltung)

- Login: Appwrite-User mit Label 'admin' (Ticketsystem-Mitarbeiter) bekommen
  eine Admin-Session und landen auf /admin.html
- Admin-Dashboard: Statistik-Uebersicht, Projektsuche ueber alle Projekte
  (inkl. Preview-/Gitea-Links, Vorschau ohne Kunden-Login), Kundenliste mit
  Portal-Status, letztem Login und Projektanzahl
- Kundennummer (code) direkt im Admin-Bereich pflegbar
- 'Als Kunde ansehen': Admin sieht das Portal exakt wie der Kunde (Banner
  mit Rueckweg zur Admin-Uebersicht)
- Kunden-Dashboard: neue Bereiche 'Meine Rechnungen' (InvoiceNinja inkl.
  Link ins Rechnungsportal) und 'Was wurde gemacht' (Tickets + Worksheets
  aus dem Ticketsystem, ohne interne Akquise-Tickets); Kundennummer im Header
- Admin-API /api/portal-admin/* (Session-basiert), Kunden-API /api/invoices
  und /api/activity
- appwriteClient: uebrig gebliebene Debug-Instrumentierung entfernt

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-11 17:09:39 +00:00
parent 0320042045
commit 177ee6a13d
12 changed files with 1085 additions and 54 deletions

View File

@@ -5,6 +5,7 @@ import {
getCustomerByEmail,
getDocument,
getPortalAccessByCustomerId,
getUserById,
listDocuments,
updateDocument,
Query,
@@ -147,6 +148,31 @@ router.post('/login', async (req, res) => {
try {
const user = await loginWithAppwrite(email.trim(), password)
// Ticketsystem-Admins (Appwrite-Label "admin") bekommen die Admin-Ansicht
const account = await getUserById(user.$id).catch(() => null)
if (Array.isArray(account?.labels) && account.labels.includes('admin')) {
const adminName = account.name || 'Admin'
const adminEmail = account.email || email.trim()
setPortalSession(res, {
role: 'admin',
appwriteUserId: user.$id,
name: adminName,
email: adminEmail,
})
const adminPreviewToken = await createPreviewSessionToken({
userId: user.$id,
customerId: '',
email: adminEmail,
name: adminName,
role: 'admin',
labels: ['admin'],
authSource: 'kundenbereich-admin',
})
res.cookie(PREVIEW_COOKIE_NAME, adminPreviewToken, previewSessionCookieOptions())
return res.json({ success: true, role: 'admin', admin: { name: adminName, email: adminEmail } })
}
let { customer, portalAccess } = await validatePortalAccess(user.$id, email.trim())
customer = await syncCustomerAppwriteUserId(customer, user.$id)
@@ -176,7 +202,7 @@ router.post('/login', async (req, res) => {
console.warn('[auth] lastLoginAt update failed:', err.message)
}
return res.json({ success: true, customer: sanitizeCustomer(customer) })
return res.json({ success: true, role: 'customer', customer: sanitizeCustomer(customer) })
} catch (err) {
const status = err.status || 500
if (status === 429) {
@@ -279,6 +305,26 @@ router.get('/me', async (req, res) => {
try {
const session = JSON.parse(raw)
// Admin-Session (Ticketsystem-Mitarbeiter), optional im "Als Kunde ansehen"-Modus
if (session.role === 'admin' && session.appwriteUserId) {
let customer = null
if (session.customerId) {
const doc = await getDocument(config.collections.customers, session.customerId).catch(() => null)
if (doc) customer = sanitizeCustomer(doc)
}
return res.json({
authenticated: true,
role: 'admin',
viewAs: Boolean(session.viewAs && customer),
admin: {
name: session.adminName || session.name || 'Admin',
email: session.adminEmail || session.email || '',
},
customer,
})
}
if (!session.customerId || !session.appwriteUserId) {
return res.json({ authenticated: false })
}
@@ -304,6 +350,7 @@ router.get('/me', async (req, res) => {
return res.json({
authenticated: true,
role: 'customer',
customer: sanitizeCustomer(customer),
})
} catch (err) {