From 96aef9b5dd9578f658e3779e66cd7f36548fe18e Mon Sep 17 00:00:00 2001 From: mAi Date: Wed, 20 May 2026 15:18:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(team-admin):=20t-paliad-223=20Slice=20B=20?= =?UTF-8?q?=E2=80=94=20Add=20User=20via=20Supabase=20Admin=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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. --- cmd/server/main.go | 14 + frontend/src/admin-team.tsx | 64 +++++ frontend/src/client/admin-team.ts | 114 +++++++++ frontend/src/client/i18n.ts | 32 +++ frontend/src/i18n-keys.ts | 16 ++ internal/handlers/admin_users.go | 72 ++++++ internal/handlers/handlers.go | 1 + internal/services/email_template_samples.go | 26 ++ internal/services/email_template_service.go | 10 + internal/services/email_template_variables.go | 26 ++ internal/services/mail_service_test.go | 47 ++++ internal/services/supabase_admin.go | 242 ++++++++++++++++++ internal/services/supabase_admin_test.go | 154 +++++++++++ internal/services/user_service.go | 212 ++++++++++++++- .../templates/email/add_user_welcome.de.html | 12 + .../templates/email/add_user_welcome.en.html | 12 + 16 files changed, 1053 insertions(+), 1 deletion(-) create mode 100644 internal/services/supabase_admin.go create mode 100644 internal/services/supabase_admin_test.go create mode 100644 internal/templates/email/add_user_welcome.de.html create mode 100644 internal/templates/email/add_user_welcome.en.html diff --git a/cmd/server/main.go b/cmd/server/main.go index 027f2b8..e7c4eec 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -128,6 +128,20 @@ func main() { inviteSvc := services.NewInviteService(pool, mailSvc, handlers.AllowedEmailDomains, baseURL) reminderSvc := services.NewReminderService(pool, mailSvc, users, baseURL) + // t-paliad-223 Slice B (#49) — Supabase Admin API client for the + // new "Konto direkt anlegen" path on /admin/team. The key is + // optional: when unset the client still wires (so dependents + // don't panic) but every call short-circuits with + // ErrSupabaseAdminUnavailable so the rest of the server stays + // runnable. + supabaseAdminClient := services.LoadSupabaseAdminClient() + if supabaseAdminClient.Enabled() { + log.Println("supabase admin API configured — /admin/team Add-User path active") + } else { + log.Println("SUPABASE_SERVICE_ROLE_KEY not set — /admin/team Add-User path will return 503") + } + users.SetAddUserDeps(supabaseAdminClient, mailSvc, baseURL) + // Wire EmailTemplateService onto the MailService so DB-backed admin // edits propagate without a process restart. The constructor is split // from MailService creation because the DB pool isn't available yet diff --git a/frontend/src/admin-team.tsx b/frontend/src/admin-team.tsx index 37fa286..8b0c253 100644 --- a/frontend/src/admin-team.tsx +++ b/frontend/src/admin-team.tsx @@ -33,6 +33,9 @@ export function renderAdminTeam(): string {

+ @@ -132,6 +135,67 @@ export function renderAdminTeam(): string {
+ {/* 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. */} +