// Ebene 1 (öffentlich) — Mitarbeiter-Selbstanmeldung über den // Sammellink eines Mandanten ("Einladung annehmen", siehe CLAUDE.md, // Onboarding: "Einladungslink [Sammellink, Selbstanmeldung, Abteilung // beim ersten Antrag]"). Ein Link gilt für beliebig viele Mitarbeiter // desselben Mandanten, kein Einmal-Token pro Person — die Abteilung // wählt der/die Mitarbeiter:in erst beim ersten Antrag (Fragebogen // Abschnitt A), nicht hier. package web import ( "net/http" "github.com/netcell-it/deklarix/internal/auth" ) type einladungFormData struct { Title string Firmenname string Error string } // handleEinladungForm zeigt das Selbstanmelde-Formular für einen // gültigen Einladungslink. Ein unbekanntes oder erneuertes (damit // ungültig gewordenes) Token bekommt dieselbe Fehlermeldung wie ein // abgelaufener Link — es wird nicht unterschieden, ob das Token nie // existiert hat oder nur nicht mehr gültig ist. func (s *Server) handleEinladungForm(w http.ResponseWriter, r *http.Request) { acc, err := s.store.GetAccountByEinladungToken(r.Context(), r.PathValue("token")) if err != nil { s.renderEinladungError(w) return } s.renderEinladungForm(w, acc.Name, "") } func (s *Server) renderEinladungForm(w http.ResponseWriter, firmenname, errMsg string) { data := einladungFormData{Title: "Einladung annehmen", Firmenname: firmenname, Error: errMsg} if err := s.templates.ExecuteTemplate(w, "einladung", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } func (s *Server) renderEinladungError(w http.ResponseWriter) { if err := s.templates.ExecuteTemplate(w, "einladung-ungueltig", authPageData{Title: "Einladung"}); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } // handleEinladungAnnehmen legt einen neuen Login mit Rolle // "mitarbeiter" im Mandanten des Einladungslinks an und meldet die // Person direkt an — wie bei der Firma-Registrierung gibt es keine // E-Mail-Bestätigung (noch nicht gebaut, siehe CLAUDE.md, Onboarding). func (s *Server) handleEinladungAnnehmen(w http.ResponseWriter, r *http.Request) { acc, err := s.store.GetAccountByEinladungToken(r.Context(), r.PathValue("token")) if err != nil { s.renderEinladungError(w) return } if err := r.ParseForm(); err != nil { s.renderEinladungForm(w, acc.Name, "ungültiges Formular") return } email := r.FormValue("email") password := r.FormValue("password") if email == "" || password == "" { s.renderEinladungForm(w, acc.Name, "E-Mail und Passwort sind Pflicht") return } passwordHash, err := auth.HashPassword(password) if err != nil { s.renderEinladungForm(w, acc.Name, err.Error()) return } user, err := s.store.CreateUser(r.Context(), acc.ID, email, passwordHash, "mitarbeiter") if err != nil { s.renderEinladungForm(w, acc.Name, "Konto konnte nicht angelegt werden — E-Mail evtl. schon vergeben") return } if err := s.startSession(w, r, user.ID); err != nil { s.renderEinladungForm(w, acc.Name, "Sitzung konnte nicht gestartet werden") return } http.Redirect(w, r, "/", http.StatusSeeOther) } type einladungVerwaltungData struct { Title string Nav navData Link string } // einladungBasisURL leitet Schema+Host aus dem Request ab, damit der // vollständige Link (nicht nur das Token) angezeigt werden kann — hinter // einem Reverse-Proxy (siehe CLAUDE.md, Testsystem: nginx+TLS) trägt // r.TLS bei HTTPS-Terminierung am Proxy nichts, deshalb zusätzlich // X-Forwarded-Proto berücksichtigen. func einladungBasisURL(r *http.Request) string { scheme := "http" if r.TLS != nil || r.Header.Get("X-Forwarded-Proto") == "https" { scheme = "https" } return scheme + "://" + r.Host } // handleEinladungVerwaltung zeigt den aktuellen Sammellink des eigenen // Mandanten zum Kopieren. func (s *Server) handleEinladungVerwaltung(w http.ResponseWriter, r *http.Request) { acc, err := s.store.GetAccount(r.Context(), currentUser(r).AccountID) if err != nil { http.Error(w, "Account konnte nicht geladen werden: "+err.Error(), http.StatusInternalServerError) return } data := einladungVerwaltungData{ Title: "Einladungslink", Nav: navFor(r), Link: einladungBasisURL(r) + "/einladung/" + acc.EinladungToken, } if err := s.templates.ExecuteTemplate(w, "einladung-verwaltung", data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } // handleEinladungErneuern ersetzt den Sammellink des eigenen Mandanten // — der alte Link wird sofort ungültig (z. B. wenn er versehentlich // außerhalb des Unternehmens geteilt wurde). func (s *Server) handleEinladungErneuern(w http.ResponseWriter, r *http.Request) { newToken, err := auth.NewSessionToken() if err != nil { http.Error(w, "Neuer Link konnte nicht erzeugt werden: "+err.Error(), http.StatusInternalServerError) return } if err := s.store.RegenerateEinladungToken(r.Context(), currentUser(r).AccountID, newToken); err != nil { http.Error(w, "Link konnte nicht erneuert werden: "+err.Error(), http.StatusInternalServerError) return } http.Redirect(w, r, "/verwaltung/einladung", http.StatusSeeOther) }