#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.
143 lines
4.7 KiB
Go
143 lines
4.7 KiB
Go
// Sample data for the /admin/email-templates preview pane. Each preview
|
|
// request renders the proposed subject + body against this fixed payload
|
|
// so the admin sees the layout exactly as it will look in production for a
|
|
// representative case. Sample data is server-authoritative; the editor
|
|
// can't override it (out of scope for v1 — see design doc §9).
|
|
package services
|
|
|
|
// EmailTemplateSampleData returns a fresh sample payload for (key, lang).
|
|
// `slot` is honoured for deadline_digest only ("morning" / "evening"); other
|
|
// keys ignore it.
|
|
func EmailTemplateSampleData(key, lang, slot string) map[string]any {
|
|
switch key {
|
|
case EmailTemplateKeyInvitation:
|
|
return invitationSample(lang)
|
|
case EmailTemplateKeyAddUserWelcome:
|
|
return addUserWelcomeSample(lang)
|
|
case EmailTemplateKeyDeadlineDigest:
|
|
return deadlineDigestSample(lang, slot)
|
|
case EmailTemplateKeyBase:
|
|
return baseSample(lang)
|
|
default:
|
|
return map[string]any{}
|
|
}
|
|
}
|
|
|
|
func invitationSample(lang string) map[string]any {
|
|
if lang == "en" {
|
|
return map[string]any{
|
|
"InviterName": "Maria Schmidt",
|
|
"InviterEmail": "maria.schmidt@hlc.com",
|
|
"ToEmail": "new.colleague@hlc.com",
|
|
"Message": "Hi — I think you'd find Paliad useful. Have a look.",
|
|
"RegisterURL": "https://paliad.de/login",
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"InviterName": "Maria Schmidt",
|
|
"InviterEmail": "maria.schmidt@hlc.com",
|
|
"ToEmail": "neu.kollege@hlc.de",
|
|
"Message": "Hallo Kolleg:in — ich glaube Paliad wäre nützlich für dich. Schau es dir an.",
|
|
"RegisterURL": "https://paliad.de/login",
|
|
}
|
|
}
|
|
|
|
func deadlineDigestSample(lang, slot string) map[string]any {
|
|
isEvening := slot == "evening"
|
|
overdue := []map[string]any{
|
|
{
|
|
"DueDate": "2026-04-27",
|
|
"Title": ifLang(lang, "Beschwerde gegen EP-Anmeldung einreichen", "File appeal against EP application"),
|
|
"ProjectReference": "HL-2024-0083",
|
|
"ProjectTitle": "Acme vs Beta GmbH",
|
|
"OwnerName": "Maria Schmidt",
|
|
"IsOtherOwner": true,
|
|
"URL": "https://paliad.de/deadlines/sample-overdue-1",
|
|
},
|
|
}
|
|
dueToday := []map[string]any{
|
|
{
|
|
"DueDate": "2026-04-29",
|
|
"Title": ifLang(lang, "Klageerwiderung einreichen", "File reply to complaint"),
|
|
"ProjectReference": "HL-2025-0011",
|
|
"ProjectTitle": "Gamma AG vs Delta Inc",
|
|
"OwnerName": "Self",
|
|
"IsOtherOwner": false,
|
|
"URL": "https://paliad.de/deadlines/sample-due-today-1",
|
|
},
|
|
{
|
|
"DueDate": "2026-04-29",
|
|
"Title": ifLang(lang, "Vollmacht prüfen und gegenzeichnen", "Review and counter-sign power of attorney"),
|
|
"ProjectReference": "HL-2025-0014",
|
|
"ProjectTitle": "",
|
|
"OwnerName": "Self",
|
|
"IsOtherOwner": false,
|
|
"URL": "https://paliad.de/deadlines/sample-due-today-2",
|
|
},
|
|
}
|
|
dueWarning := []map[string]any{
|
|
{
|
|
"DueDate": "2026-05-06",
|
|
"Title": ifLang(lang, "Stellungnahme zur Erteilung vorbereiten", "Prepare response to grant"),
|
|
"ProjectReference": "HL-2025-0007",
|
|
"ProjectTitle": "Epsilon Ltd",
|
|
"OwnerName": "Self",
|
|
"IsOtherOwner": false,
|
|
"URL": "https://paliad.de/deadlines/sample-warning-1",
|
|
},
|
|
}
|
|
return map[string]any{
|
|
"Slot": slot,
|
|
"IsEvening": isEvening,
|
|
"Overdue": overdue,
|
|
"OverdueCount": len(overdue),
|
|
"DueToday": dueToday,
|
|
"DueTodayCount": len(dueToday),
|
|
"DueWarning": dueWarning,
|
|
"DueWarningCount": len(dueWarning),
|
|
"OpenTotal": len(dueToday) + len(dueWarning),
|
|
"DeadlinesURL": "https://paliad.de/deadlines",
|
|
}
|
|
}
|
|
|
|
// t-paliad-223 Slice B (#49) — sample data for the Add-User welcome mail.
|
|
// The variable contract mirrors what UserService.AdminCreateUserFull
|
|
// passes to MailService.SendTemplate at runtime.
|
|
func addUserWelcomeSample(lang string) map[string]any {
|
|
if lang == "en" {
|
|
return map[string]any{
|
|
"InviterName": "Maria Schmidt",
|
|
"InviterEmail": "maria.schmidt@hlc.com",
|
|
"ToEmail": "new.colleague@hlc.com",
|
|
"MagicLink": "https://supabase.paliad.de/auth/v1/verify?token=…",
|
|
"BaseURL": "https://paliad.de",
|
|
"Firm": "HLC",
|
|
}
|
|
}
|
|
return map[string]any{
|
|
"InviterName": "Maria Schmidt",
|
|
"InviterEmail": "maria.schmidt@hlc.com",
|
|
"ToEmail": "neu.kollege@hlc.de",
|
|
"MagicLink": "https://supabase.paliad.de/auth/v1/verify?token=…",
|
|
"BaseURL": "https://paliad.de",
|
|
"Firm": "HLC",
|
|
}
|
|
}
|
|
|
|
func baseSample(lang string) map[string]any {
|
|
subj := "Beispielbetreff"
|
|
if lang == "en" {
|
|
subj = "Example subject"
|
|
}
|
|
return map[string]any{
|
|
"Subject": subj,
|
|
}
|
|
}
|
|
|
|
func ifLang(lang, de, en string) string {
|
|
if lang == "en" {
|
|
return en
|
|
}
|
|
return de
|
|
}
|