#49 — adds a third "Konto direkt anlegen" path on /admin/team alongside "Onboard existing" and "Invite colleague". Creates both auth.users (via Supabase Admin API) and paliad.users in one click; new user is visible in dropdowns immediately and receives a paliad-branded magic-link email. - internal/services/supabase_admin.go: new SupabaseAdminClient — thin net/http shim. 3 methods (CreateAuthUser, GenerateRecoveryLink, DeleteAuthUser). 10s timeout. ErrSupabaseAdminUnavailable when key unset, ErrSupabaseEmailExists when 422-with-"already" returned. apikey + Bearer headers on every call. Sentinel errors for handler mapping. - internal/services/supabase_admin_test.go: 5 tests pin wire-shape (disabled mode, happy-path POST + headers + body, email-exists mapping, both action-link response shapes, DELETE-by-id route). - internal/services/user_service.go: UserService grows optional supabase + mail + baseURL dependencies via SetAddUserDeps. AdminCreateFullInput (email/display_name/office/job_title/profession/lang/send_welcome_mail + inviter fields). AdminCreateUserFull validates input → calls supabase.CreateAuthUser → inserts paliad.users (best-effort DeleteAuthUser rollback on insert fail) → writes paliad.system_audit_log row (event_type='user.added_by_admin') → sends welcome mail with magic-link (best-effort). - internal/templates/email/add_user_welcome.{de,en}.html: new template with magic-link CTA + base-URL fallback + firm-name placeholder. Editable through the existing /admin/email-templates editor (admin-overridable via DB). - internal/services/email_template_*.go: register 'add_user_welcome' as a fourth canonical key, defaultSubjects entry, sample data, variable contract (6 vars). - internal/services/mail_service_test.go: TestRenderTemplateAddUserWelcome pins both langs render with magic-link + firm + matching subject. - internal/handlers/admin_users.go: handleAdminCreateFullUser POST /api/admin/users/full. Fills inviter fields from auth.uid() server-side (never trusts the request body). Error map: 503 (unavailable), 409 (email exists / already onboarded), 400 (invalid input), 403 (domain not on whitelist), 500 (other). - internal/handlers/handlers.go: route registered behind adminGate. - cmd/server/main.go: LoadSupabaseAdminClient + users.SetAddUserDeps + boot-log line so the deployer knows whether the path is active. - frontend/src/admin-team.tsx: "Konto direkt anlegen" button + admin-add-full-modal with email/name/office/profession/job_title/lang fields + send-welcome checkbox (default on). - frontend/src/client/admin-team.ts: initAddFullModal — POST to /api/admin/users/full, inline error handling for 503 / 409 / generic, optimistic insert into users[] on success, name auto-fills from email local-part on blur. - i18n: +20 keys (admin.team.add.full + admin.team.add_full.*) × DE + EN. Design picks honoured: Supabase Admin API path (Q1), welcome email default on (Q2), two-step with best-effort rollback (Q3), job_title default 'Associate' (Q4), profession default 'associate' (Q5). Trade-off #3 from §6 (privileged credential broadens trust surface) accepted by m via head. go build && go test -short ./internal/... + bun run build all green.
206 lines
11 KiB
TypeScript
206 lines
11 KiB
TypeScript
import { h } from "./jsx";
|
|
import { Sidebar } from "./components/Sidebar";
|
|
import { PaliadinWidget } from "./components/PaliadinWidget";
|
|
import { BottomNav } from "./components/BottomNav";
|
|
import { Footer } from "./components/Footer";
|
|
import { PWAHead } from "./components/PWAHead";
|
|
|
|
export function renderAdminTeam(): string {
|
|
return "<!DOCTYPE html>" + (
|
|
<html lang="de">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
|
<meta name="theme-color" content="#BFF355" />
|
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
|
<PWAHead />
|
|
<title data-i18n="admin.team.title">Team-Verwaltung — Paliad</title>
|
|
<link rel="stylesheet" href="/assets/global.css" />
|
|
</head>
|
|
<body className="has-sidebar">
|
|
<Sidebar currentPath="/admin/team" />
|
|
<BottomNav currentPath="/admin/team" />
|
|
|
|
<main>
|
|
<section className="tool-page">
|
|
<div className="container">
|
|
<div className="tool-header">
|
|
<div>
|
|
<h1 data-i18n="admin.team.heading">Team-Verwaltung</h1>
|
|
<p className="tool-subtitle" data-i18n="admin.team.subtitle">
|
|
Alle Paliad-Konten anzeigen, bearbeiten oder hinzufügen.
|
|
</p>
|
|
</div>
|
|
<div className="admin-team-actions">
|
|
<button className="btn-primary" id="admin-team-add-full" type="button" data-i18n="admin.team.add.full">
|
|
Konto direkt anlegen
|
|
</button>
|
|
<button className="btn-primary" id="admin-team-direct-add" type="button" data-i18n="admin.team.add.direct">
|
|
Bestehendes Konto onboarden
|
|
</button>
|
|
<button className="btn-primary" id="admin-team-invite" type="button" data-i18n="admin.team.add.invite">
|
|
Neue:n Kolleg:in einladen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="admin-team-controls">
|
|
<div className="glossar-search-wrap">
|
|
<svg className="glossar-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="11" cy="11" r="8" />
|
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
</svg>
|
|
<input
|
|
type="text"
|
|
id="admin-team-search"
|
|
className="glossar-search"
|
|
placeholder="Nach Name oder E-Mail suchen..."
|
|
data-i18n-placeholder="admin.team.search.placeholder"
|
|
autocomplete="off"
|
|
/>
|
|
<span className="glossar-count" id="admin-team-count" />
|
|
</div>
|
|
<div className="admin-team-filter-row" id="admin-team-office-filters">
|
|
<button className="filter-pill active" data-office="all" type="button" data-i18n="team.filter.all">Alle</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="admin-team-feedback" className="form-msg" style="display:none" />
|
|
|
|
<div className="entity-table-wrap admin-team-table-wrap">
|
|
<table className="entity-table entity-table--readonly admin-team-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-i18n="admin.team.col.name">Name</th>
|
|
<th data-i18n="admin.team.col.email">E-Mail</th>
|
|
<th data-i18n="admin.team.col.office">Standort</th>
|
|
<th data-i18n="admin.team.col.job_title">Berufsbezeichnung</th>
|
|
<th data-i18n="admin.team.col.profession">Profession</th>
|
|
<th data-i18n="admin.team.col.permission">Berechtigung</th>
|
|
<th data-i18n="admin.team.col.additional">Weitere Standorte</th>
|
|
<th data-i18n="admin.team.col.lang">Sprache</th>
|
|
<th data-i18n="admin.team.col.created">Angelegt</th>
|
|
<th data-i18n="admin.team.col.actions">Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="admin-team-tbody">
|
|
<tr><td colspan={10} className="admin-team-loading" data-i18n="admin.team.loading">Lade...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div className="entity-empty" id="admin-team-empty" style="display:none">
|
|
<p data-i18n="admin.team.empty">Keine Treffer.</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
|
|
{/* Direct-add modal: pick from unonboarded auth.users dropdown. */}
|
|
<div className="modal-overlay" id="admin-direct-add-modal" style="display:none">
|
|
<div className="modal-card">
|
|
<div className="modal-header">
|
|
<h2 data-i18n="admin.team.direct_add.title">Bestehendes Konto onboarden</h2>
|
|
<button className="modal-close" id="admin-direct-add-close" type="button" aria-label="Close">×</button>
|
|
</div>
|
|
<p data-i18n="admin.team.direct_add.body" className="invite-modal-body">
|
|
Diese Auswahl zeigt Konten, die sich angemeldet haben, aber noch kein Profil ausgefüllt haben.
|
|
</p>
|
|
<form id="admin-direct-add-form" className="entity-form" autocomplete="off">
|
|
<div className="form-field">
|
|
<label htmlFor="admin-da-email" data-i18n="admin.team.direct_add.email">E-Mail</label>
|
|
<select id="admin-da-email" name="email" required>
|
|
<option value="" data-i18n="admin.team.direct_add.email.placeholder">Bitte auswählen...</option>
|
|
</select>
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="admin-da-name" data-i18n="admin.team.direct_add.name">Anzeigename</label>
|
|
<input type="text" id="admin-da-name" name="display_name" required />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="admin-da-office" data-i18n="admin.team.direct_add.office">Standort</label>
|
|
<select id="admin-da-office" name="office" required />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="admin-da-role" data-i18n="admin.team.direct_add.job_title">Berufsbezeichnung</label>
|
|
<input type="text" id="admin-da-role" name="job_title" placeholder="Associate" />
|
|
</div>
|
|
<div id="admin-da-feedback" className="form-msg" style="display:none" />
|
|
<div className="form-actions">
|
|
<button type="button" className="btn-cancel" id="admin-da-cancel" data-i18n="admin.team.direct_add.cancel">Abbrechen</button>
|
|
<button type="submit" className="btn-primary" id="admin-da-submit" data-i18n="admin.team.direct_add.submit">Anlegen</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{/* t-paliad-223 Slice B (#49) — "Konto direkt anlegen" modal.
|
|
Creates BOTH the auth.users row (via Supabase Admin API) and
|
|
the paliad.users row in one click. New user is visible in
|
|
dropdowns immediately. */}
|
|
<div className="modal-overlay" id="admin-add-full-modal" style="display:none">
|
|
<div className="modal-card">
|
|
<div className="modal-header">
|
|
<h2 data-i18n="admin.team.add_full.title">Konto direkt anlegen</h2>
|
|
<button className="modal-close" id="admin-af-close" type="button" aria-label="Close">×</button>
|
|
</div>
|
|
<p data-i18n="admin.team.add_full.body" className="invite-modal-body">
|
|
Legt sowohl das Login-Konto als auch das Paliad-Profil an. Die neue Person erhält eine E-Mail mit einem Link, über den sie ein Passwort setzt.
|
|
</p>
|
|
<form id="admin-add-full-form" className="entity-form" autocomplete="off">
|
|
<div className="form-field">
|
|
<label htmlFor="admin-af-email" data-i18n="admin.team.add_full.email">E-Mail</label>
|
|
<input type="email" id="admin-af-email" name="email" required autocomplete="off" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="admin-af-name" data-i18n="admin.team.add_full.name">Anzeigename</label>
|
|
<input type="text" id="admin-af-name" name="display_name" required />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="admin-af-office" data-i18n="admin.team.add_full.office">Standort</label>
|
|
<select id="admin-af-office" name="office" required />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="admin-af-profession" data-i18n="admin.team.add_full.profession">Profession</label>
|
|
<select id="admin-af-profession" name="profession">
|
|
<option value="partner" data-i18n="projects.team.profession.partner">Partner</option>
|
|
<option value="of_counsel" data-i18n="projects.team.profession.of_counsel">Of Counsel</option>
|
|
<option value="associate" selected data-i18n="projects.team.profession.associate">Associate</option>
|
|
<option value="senior_pa" data-i18n="projects.team.profession.senior_pa">Senior PA</option>
|
|
<option value="pa" data-i18n="projects.team.profession.pa">PA</option>
|
|
<option value="paralegal" data-i18n="projects.team.profession.paralegal">Paralegal</option>
|
|
</select>
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="admin-af-job-title" data-i18n="admin.team.add_full.job_title">Berufsbezeichnung</label>
|
|
<input type="text" id="admin-af-job-title" name="job_title" placeholder="Associate" />
|
|
</div>
|
|
<div className="form-field">
|
|
<label htmlFor="admin-af-lang" data-i18n="admin.team.add_full.lang">Sprache</label>
|
|
<select id="admin-af-lang" name="lang">
|
|
<option value="de" selected>Deutsch</option>
|
|
<option value="en">English</option>
|
|
</select>
|
|
</div>
|
|
<label className="form-checkbox">
|
|
<input type="checkbox" id="admin-af-send-welcome" checked />
|
|
<span data-i18n="admin.team.add_full.send_welcome">Willkommens-E-Mail mit Login-Link senden</span>
|
|
</label>
|
|
<div id="admin-af-feedback" className="form-msg" style="display:none" />
|
|
<div className="form-actions">
|
|
<button type="button" className="btn-cancel" id="admin-af-cancel" data-i18n="admin.team.add_full.cancel">Abbrechen</button>
|
|
<button type="submit" className="btn-primary" id="admin-af-submit" data-i18n="admin.team.add_full.submit">Anlegen</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<Footer />
|
|
<PaliadinWidget />
|
|
<script src="/assets/admin-team.js"></script>
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|