package app import ( "net/http" "strings" "time" "github.com/yourorg/ntfywui/internal/security" "github.com/yourorg/ntfywui/internal/store" ) func (s *Server) handleAdmins(w http.ResponseWriter, r *http.Request) { admin, _ := s.currentAdmin(r) switch r.Method { case http.MethodGet: csrf, _ := s.csrfEnsure(w, r) flash := s.popFlash(w, r) s.renderer.Render(w, "admins.html", PageData{ Title: "Admins", Admin: admin.Username, Role: string(admin.Role), CSRF: csrf, Flash: flash, Admins: s.admins.List(), }) case http.MethodPost: _ = r.ParseForm() action := r.Form.Get("action") username := cleanUser(r.Form.Get("username")) switch action { case "create": pass := r.Form.Get("password") role := store.Role(strings.TrimSpace(r.Form.Get("role"))) if role == "" { role = store.RoleOperator } if username == "" || pass == "" { s.setFlash(w, r, "Username und Passwort erforderlich") http.Redirect(w, r, s.abs("/admins"), http.StatusFound) return } if role != store.RoleViewer && role != store.RoleOperator && role != store.RoleAdmin { s.setFlash(w, r, "Ungültige Rolle") http.Redirect(w, r, s.abs("/admins"), http.StatusFound) return } if _, ok := s.admins.Get(username); ok { s.setFlash(w, r, "Admin existiert bereits") http.Redirect(w, r, s.abs("/admins"), http.StatusFound) return } salt := make([]byte, 16) _, _ = randRead(salt) hash := security.HashPasswordPBKDF2(pass, salt, 120_000) a := store.Admin{ Username: username, Role: role, PassHash: hash, CreatedAt: time.Now().Unix(), } if err := s.admins.Set(a); err != nil { s.setFlash(w, r, "Fehler: "+err.Error()) } else { s.auditEvent(r, "webui_admin_create", username, map[string]string{"role": string(role)}) s.setFlash(w, r, "Admin erstellt") } http.Redirect(w, r, s.abs("/admins"), http.StatusFound) case "set-role": role := store.Role(strings.TrimSpace(r.Form.Get("role"))) a, ok := s.admins.Get(username) if !ok { http.NotFound(w, r) return } if role != store.RoleViewer && role != store.RoleOperator && role != store.RoleAdmin { s.setFlash(w, r, "Ungültige Rolle") http.Redirect(w, r, s.abs("/admins"), http.StatusFound) return } a.Role = role if err := s.admins.Set(a); err != nil { s.setFlash(w, r, "Fehler: "+err.Error()) } else { s.auditEvent(r, "webui_admin_set_role", username, map[string]string{"role": string(role)}) s.setFlash(w, r, "Rolle geändert") } http.Redirect(w, r, s.abs("/admins"), http.StatusFound) case "set-pass": pass := r.Form.Get("password") if pass == "" { s.setFlash(w, r, "Passwort erforderlich") http.Redirect(w, r, s.abs("/admins"), http.StatusFound) return } a, ok := s.admins.Get(username) if !ok { http.NotFound(w, r) return } salt := make([]byte, 16) _, _ = randRead(salt) a.PassHash = security.HashPasswordPBKDF2(pass, salt, 120_000) if err := s.admins.Set(a); err != nil { s.setFlash(w, r, "Fehler: "+err.Error()) } else { s.auditEvent(r, "webui_admin_set_pass", username, nil) s.setFlash(w, r, "Passwort geändert") } http.Redirect(w, r, s.abs("/admins"), http.StatusFound) case "toggle-disable": a, ok := s.admins.Get(username) if !ok { http.NotFound(w, r) return } // Prevent self-lockout if username == admin.Username { s.setFlash(w, r, "Du kannst dich nicht selbst deaktivieren") http.Redirect(w, r, s.abs("/admins"), http.StatusFound) return } a.Disabled = !a.Disabled if err := s.admins.Set(a); err != nil { s.setFlash(w, r, "Fehler: "+err.Error()) } else { s.auditEvent(r, "webui_admin_toggle_disable", username, map[string]string{"disabled": boolStr(a.Disabled)}) s.setFlash(w, r, "Aktualisiert") } http.Redirect(w, r, s.abs("/admins"), http.StatusFound) case "2fa-enable": a, ok := s.admins.Get(username) if !ok { http.NotFound(w, r) return } secret, err := security.GenerateTOTPSecret() if err != nil { s.setFlash(w, r, "Fehler: "+err.Error()) http.Redirect(w, r, s.abs("/admins"), http.StatusFound) return } a.TOTPSecret = secret if err := s.admins.Set(a); err != nil { s.setFlash(w, r, "Fehler: "+err.Error()) } else { s.auditEvent(r, "webui_admin_2fa_enable", username, nil) s.setFlash(w, r, "2FA Secret (speichern!): "+secret) } http.Redirect(w, r, s.abs("/admins"), http.StatusFound) case "2fa-disable": a, ok := s.admins.Get(username) if !ok { http.NotFound(w, r) return } a.TOTPSecret = "" if err := s.admins.Set(a); err != nil { s.setFlash(w, r, "Fehler: "+err.Error()) } else { s.auditEvent(r, "webui_admin_2fa_disable", username, nil) s.setFlash(w, r, "2FA deaktiviert") } http.Redirect(w, r, s.abs("/admins"), http.StatusFound) case "delete": // Prevent deleting self if username == admin.Username { s.setFlash(w, r, "Du kannst dich nicht selbst löschen") http.Redirect(w, r, s.abs("/admins"), http.StatusFound) return } if err := s.admins.Delete(username); err != nil { s.setFlash(w, r, "Fehler: "+err.Error()) } else { s.auditEvent(r, "webui_admin_delete", username, nil) s.setFlash(w, r, "Admin gelöscht") } http.Redirect(w, r, s.abs("/admins"), http.StatusFound) default: http.Error(w, "bad request", http.StatusBadRequest) } default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func boolStr(b bool) string { if b { return "true" } return "false" }