package app import ( "net/http" "strings" "github.com/yourorg/ntfywui/internal/security" "github.com/yourorg/ntfywui/internal/store" ) func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: next := r.URL.Query().Get("next") if next == "" { next = "/users" } csrf, _ := s.csrfEnsure(w, r) flash := s.popFlash(w, r) s.renderer.Render(w, "login.html", PageData{ Title: "Login", CSRF: csrf, Flash: flash, Next: next, }) case http.MethodPost: _ = r.ParseForm() user := cleanUser(r.Form.Get("username")) pass := r.Form.Get("password") totp := strings.TrimSpace(r.Form.Get("totp")) next := r.Form.Get("next") if next == "" { next = "/users" } // CSRF checked by middleware in routes (we add it by calling s.csrf wrapper above in routes) a, ok := s.admins.Authenticate(user, pass, totp) if !ok { s.audit.Append(store.AuditEvent{ Actor: user, IP: security.RealIP(r, security.RealIPConfig{TrustedProxies: s.cfg.TrustedProxies}), UA: r.UserAgent(), Action: "login_failed", }) s.setFlash(w, r, "Login fehlgeschlagen") http.Redirect(w, r, s.abs("/login"), http.StatusFound) return } sess, _ := s.sessions.Get(r) if sess == nil { sess = &security.Session{} } sess.User = a.Username sess.Role = string(a.Role) if sess.CSRF == "" { tok, _ := security.NewCSRFToken() sess.CSRF = tok } s.sessions.Save(w, sess) s.auditEvent(r, "login_ok", a.Username, map[string]string{"role": string(a.Role)}) http.Redirect(w, r, s.abs(next), http.StatusFound) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { s.auditEvent(r, "logout", "", nil) s.sessions.Clear(w) http.Redirect(w, r, s.abs("/login"), http.StatusFound) } func (s *Server) csrfEnsure(w http.ResponseWriter, r *http.Request) (string, error) { sess, ok := s.sessions.Get(r) if !ok { sess = &security.Session{} } if sess.CSRF == "" { tok, err := security.NewCSRFToken() if err != nil { return "", err } sess.CSRF = tok _ = s.sessions.Save(w, sess) } return sess.CSRF, nil }