Files
paliad/internal/handlers/auth.go
m 495e519475 feat(t-paliad-065): firm-agnostic branding via single FIRM_NAME constant
Paliad ships firm-agnostic per CLAUDE.md ("survives firm renames") but
landing copy, email templates, page titles, and form placeholders still
hard-coded "Hogan Lovells" / "HL Patents". Replaces every user-facing
firm reference with a single source of truth: internal/branding.Name on
the server and frontend/src/branding.ts in the bundle, both reading
FIRM_NAME at startup/build time and defaulting to "HLC".

Server: branding package + boot log; auth, invite, admin_users error
strings; courts/offices/models comments; mail templates thread
{{.Firm}} via injected payload default. Files handler keeps the
upstream "HL Patents Style.dotm" path (must match mWorkRepo's blob
name) but renders the user-visible DownloadName from branding.Name.

Frontend: branding.ts read via Bun.build define so process.env.FIRM_NAME
is statically substituted into client bundles (no runtime process
reference); index/login/downloads/kostenrechner/Sidebar/ProjectFormFields
and every i18n.ts string templated against ${FIRM}.

ALLOWED_EMAIL_DOMAINS whitelist intentionally untouched — email
domains and display name rotate independently.

Verified: go build/vet/test clean; bun run build clean; FIRM_NAME=Acme
override produces "Acme" in HTML and JS bundles end-to-end.
2026-04-28 22:44:06 +02:00

149 lines
4.6 KiB
Go

package handlers
import (
"encoding/json"
"log"
"net/http"
"os"
"strings"
"mgit.msbls.de/m/patholo/internal/auth"
"mgit.msbls.de/m/patholo/internal/branding"
)
func handleLoginPage(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(auth.SessionCookieName); err == nil && cookie.Value != "" {
if _, err := authClient.VerifyToken(cookie.Value); err == nil {
http.Redirect(w, r, "/", http.StatusFound)
return
}
}
http.ServeFile(w, r, "dist/login.html")
}
func handleAPILogin(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage."})
return
}
req.Email = strings.TrimSpace(req.Email)
if req.Email == "" || req.Password == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Bitte E-Mail und Passwort eingeben."})
return
}
if !isAllowedEmailDomain(req.Email) {
writeJSON(w, http.StatusForbidden, map[string]string{"error": "Zugang nur für autorisierte " + branding.Name + "-E-Mail-Adressen."})
return
}
tokens, err := authClient.SignIn(req.Email, req.Password)
if err != nil {
log.Printf("sign in failed for %s: %v", req.Email, err)
errMsg := "Anmeldung fehlgeschlagen. Bitte versuchen Sie es erneut."
if strings.Contains(err.Error(), "Invalid login credentials") {
errMsg = "Ungültige E-Mail-Adresse oder Passwort."
}
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": errMsg})
return
}
auth.SetAuthCookies(w, r, tokens)
writeJSON(w, http.StatusOK, map[string]string{"ok": "true"})
}
func handleAPIRegister(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Ungültige Anfrage."})
return
}
req.Email = strings.TrimSpace(req.Email)
if req.Email == "" || req.Password == "" {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Bitte alle Felder ausfüllen."})
return
}
if len(req.Password) < 8 {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Passwort muss mindestens 8 Zeichen lang sein."})
return
}
if !isAllowedEmailDomain(req.Email) {
writeJSON(w, http.StatusForbidden, map[string]string{"error": "Registrierung nur für autorisierte " + branding.Name + "-E-Mail-Adressen."})
return
}
tokens, err := authClient.SignUp(req.Email, req.Password)
if err != nil {
log.Printf("sign up failed for %s: %v", req.Email, err)
errMsg := "Registrierung fehlgeschlagen. Bitte versuchen Sie es erneut."
if strings.Contains(err.Error(), "already registered") || strings.Contains(err.Error(), "already been registered") {
errMsg = "Ein Account mit dieser E-Mail existiert bereits."
}
writeJSON(w, http.StatusBadRequest, map[string]string{"error": errMsg})
return
}
if tokens != nil {
auth.SetAuthCookies(w, r, tokens)
writeJSON(w, http.StatusOK, map[string]string{"redirect": "/"})
return
}
writeJSON(w, http.StatusOK, map[string]string{"message": "Account erstellt. Bitte melden Sie sich an."})
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(auth.SessionCookieName); err == nil {
authClient.SignOut(cookie.Value)
}
auth.ClearAuthCookies(w)
http.Redirect(w, r, "/login", http.StatusFound)
}
// isAllowedEmailDomain gates sign-in/register to the firm's email domains.
// Whitelist is configurable via ALLOWED_EMAIL_DOMAINS (comma-separated),
// defaulting to hoganlovells.com,hlc.com,hlc.de so legacy and post-merger
// addresses keep working until IT finishes the domain consolidation.
// Note: this whitelist intentionally references real DNS domains, not
// branding.Name — the firm's email domains and the firm's display name are
// separate concerns and rotate on different cadences.
func isAllowedEmailDomain(email string) bool {
parts := strings.SplitN(email, "@", 2)
if len(parts) != 2 {
return false
}
domain := strings.ToLower(parts[1])
for _, allowed := range AllowedEmailDomains() {
if domain == allowed {
return true
}
}
return false
}
func AllowedEmailDomains() []string {
raw := os.Getenv("ALLOWED_EMAIL_DOMAINS")
if strings.TrimSpace(raw) == "" {
return []string{"hoganlovells.com", "hlc.com", "hlc.de"}
}
var out []string
for _, d := range strings.Split(raw, ",") {
d = strings.TrimSpace(strings.ToLower(d))
if d != "" {
out = append(out, d)
}
}
return out
}