package web import ( "net/http" "time" "github.com/netcell-it/deklarix/internal/auth" ) type authPageData struct { Title string Error string } func (s *Server) renderAuthPage(w http.ResponseWriter, name string, data authPageData) { if err := s.templates.ExecuteTemplate(w, name, data); err != nil { http.Error(w, "Seite konnte nicht gerendert werden", http.StatusInternalServerError) } } func (s *Server) handleRegisterForm(w http.ResponseWriter, r *http.Request) { s.renderAuthPage(w, "register", authPageData{Title: "Registrieren"}) } func (s *Server) handleLoginForm(w http.ResponseWriter, r *http.Request) { s.renderAuthPage(w, "login", authPageData{Title: "Anmelden"}) } // handleRegister ist die "Firma"-Registrierung (Ebene 1 der Frontend- // Spezifikation): legt einen neuen Mandanten (Account) und den ersten // Nutzer als admin an. Testzugang ist sofort aktiv, der Bezahlbetrieb // wird separat vom Betreiber freigeschaltet (nicht Teil dieses Flows). // Es gibt bewusst KEINE offene Selbstregistrierung für einzelne // Mitarbeiter — die kommen über Einladungslink/CSV-Import/Mandantenlink // (spätere Ausbaustufe), nicht über dieses Formular. func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { s.renderAuthPage(w, "register", authPageData{Title: "Registrieren", Error: "ungültiges Formular"}) return } accountName := r.FormValue("account_name") email := r.FormValue("email") password := r.FormValue("password") if accountName == "" || email == "" || password == "" { s.renderAuthPage(w, "register", authPageData{Title: "Registrieren", Error: "Alle Felder sind Pflicht"}) return } passwordHash, err := auth.HashPassword(password) if err != nil { s.renderAuthPage(w, "register", authPageData{Title: "Registrieren", Error: err.Error()}) return } ctx := r.Context() acc, err := s.store.CreateAccount(ctx, accountName) if err != nil { s.renderAuthPage(w, "register", authPageData{Title: "Registrieren", Error: "Konto konnte nicht angelegt werden"}) return } user, err := s.store.CreateUser(ctx, acc.ID, email, passwordHash, "admin") if err != nil { s.renderAuthPage(w, "register", authPageData{Title: "Registrieren", Error: "Nutzer konnte nicht angelegt werden — E-Mail evtl. schon vergeben"}) return } if err := s.startSession(w, r, user.ID); err != nil { s.renderAuthPage(w, "register", authPageData{Title: "Registrieren", Error: "Sitzung konnte nicht gestartet werden"}) return } http.Redirect(w, r, "/", http.StatusSeeOther) } func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { s.renderAuthPage(w, "login", authPageData{Title: "Anmelden", Error: "ungültiges Formular"}) return } email := r.FormValue("email") password := r.FormValue("password") ctx := r.Context() user, err := s.store.GetUserByEmail(ctx, email) if err != nil { // Absichtlich dieselbe Meldung wie bei falschem Passwort — sonst // verrät die Fehlermeldung, ob eine E-Mail-Adresse existiert. s.renderAuthPage(w, "login", authPageData{Title: "Anmelden", Error: "E-Mail oder Passwort falsch"}) return } if err := auth.VerifyPassword(user.PasswordHash, password); err != nil { s.renderAuthPage(w, "login", authPageData{Title: "Anmelden", Error: "E-Mail oder Passwort falsch"}) return } // Erst NACH erfolgreicher Passwortprüfung unterscheiden — sonst würde // diese Meldung einem Angreifer ohne gültiges Passwort verraten, dass // die E-Mail-Adresse zu einem (deaktivierten) Konto gehört. if !user.Active { s.renderAuthPage(w, "login", authPageData{Title: "Anmelden", Error: "Dieses Konto ist deaktiviert"}) return } if err := s.startSession(w, r, user.ID); err != nil { s.renderAuthPage(w, "login", authPageData{Title: "Anmelden", Error: "Sitzung konnte nicht gestartet werden"}) return } // Ein Betreiber-Login hat keinen eigenen Antrag zu stellen — der // Plattform-Bereich ist der sinnvolle Einstieg (Ebene 5, technisch // getrennt vom Mandantenbereich). if user.Role == "betreiber" { http.Redirect(w, r, "/betreiber", http.StatusSeeOther) return } http.Redirect(w, r, "/", http.StatusSeeOther) } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie(sessionCookieName); err == nil { _ = s.store.DeleteSession(r.Context(), cookie.Value) } http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1, }) http.Redirect(w, r, "/login", http.StatusSeeOther) } // startSession erzeugt eine neue Sitzung und setzt das Session-Cookie. // Secure ist an genau dann, wenn die Anfrage selbst über TLS kam — ein // fest verdrahtetes Secure=true würde lokale Entwicklung ohne TLS // (http://localhost) brechen, da Browser ein Secure-Cookie über Klartext- // HTTP schlicht nicht speichern. func (s *Server) startSession(w http.ResponseWriter, r *http.Request, userID string) error { token, err := auth.NewSessionToken() if err != nil { return err } expiresAt := time.Now().Add(auth.SessionDuration) if _, err := s.store.CreateSession(r.Context(), token, userID, expiresAt); err != nil { return err } http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, Value: token, Path: "/", HttpOnly: true, Secure: r.TLS != nil, SameSite: http.SameSiteStrictMode, Expires: expiresAt, }) return nil }