init
This commit is contained in:
199
internal/app/handlers_admins.go
Normal file
199
internal/app/handlers_admins.go
Normal file
@@ -0,0 +1,199 @@
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user