// main.go // Go 1.21+ // Web-App mit Login, Persistenz (JSON-Datei), Audit-Log, Nutzerverwaltung // und UI ohne externe Verweise. Benutzer können "Benutzernamen" melden; // Einträge starten mit Status "Gemeldet" und müssen von einem anderen Nutzer // bestätigt werden. Alles wird auditiert. // // PRODUKTIONSHINWEISE: HTTPS erzwingen (Secure-Cookie), CSRF-Schutz, // bcrypt/argon2id statt Demo-Hash, Rate Limiting, echte DB, Passwort-Richtlinien. package main import ( "context" "crypto/hmac" crand "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/json" "errors" "fmt" "html/template" "log" mrand "math/rand" "net/http" "os" "path/filepath" "strconv" "strings" "sync" "time" ) // ======= Datenmodelle ======= type User struct { Username string `json:"username"` PasswordHash string `json:"password_hash"` IsAdmin bool `json:"is_admin"` CreatedAt int64 `json:"created_at"` } type Status string const ( StatusGemeldet Status = "Gemeldet" StatusBestaetigt Status = "Bestätigt" ) type Report struct { ID int64 `json:"id"` Name string `json:"name"` Status Status `json:"status"` ReportedBy string `json:"reported_by"` ConfirmedBy string `json:"confirmed_by"` CreatedAt int64 `json:"created_at"` ConfirmedAt int64 `json:"confirmed_at"` Reasons []Reason `json:"reasons"` } type Reason struct { ID int64 `json:"id"` Text string `json:"text"` AddedBy string `json:"added_by"` AddedAt int64 `json:"added_at"` } type AuditEntry struct { Time int64 `json:"time"` Actor string `json:"actor"` Action string `json:"action"` Details string `json:"details"` IP string `json:"ip"` } type DB struct { Users []User `json:"users"` Reports []Report `json:"reports"` Audit []AuditEntry `json:"audit"` NextID int64 `json:"next_id"` NextRID int64 `json:"next_rid"` } // ======= Globale Variablen ======= var ( dataPath = envOr("DATA_FILE", "data.json") cookieName = envOr("COOKIE_NAME", "session") secretKey []byte // HMAC für Session-Cookies mu sync.Mutex db DB tpl *template.Template ) // ======= Helpers: Datei + Env ======= func envOr(k, def string) string { if v := os.Getenv(k); v != "" { return v } return def } func loadDB() error { mu.Lock() defer mu.Unlock() f, err := os.Open(dataPath) if err != nil { if os.IsNotExist(err) { db = DB{NextID: 1, NextRID: 1} return saveDB() } return err } defer f.Close() dec := json.NewDecoder(f) return dec.Decode(&db) } func saveDB() error { f, err := os.CreateTemp(filepath.Dir(dataPath), "dbtmp-*.json") if err != nil { return err } enc := json.NewEncoder(f) enc.SetIndent("", " ") if err := enc.Encode(db); err != nil { f.Close() os.Remove(f.Name()) return err } f.Close() return os.Rename(f.Name(), dataPath) } // ======= Passwort-Hash (Demo!) ======= func hashPassword(pw string, salt []byte) string { h := sha256.New() h.Write(salt) h.Write([]byte(pw)) for i := 0; i < 100_000; i++ { h.Write([]byte{byte(i)}) } return base64.StdEncoding.EncodeToString(h.Sum(nil)) + ":" + base64.StdEncoding.EncodeToString(salt) } func verifyPassword(pw, stored string) bool { parts := strings.Split(stored, ":") if len(parts) != 2 { return false } salt, err := base64.StdEncoding.DecodeString(parts[1]) if err != nil { return false } return hashPassword(pw, salt) == stored } func newSalt() []byte { b := make([]byte, 16); _, _ = crand.Read(b); return b } // ======= Sessions ======= type ctxKey string var userKey ctxKey = "user" type Session struct { Username string Expires int64 } func sign(data []byte) string { mac := hmac.New(sha256.New, secretKey) mac.Write(data) return base64.StdEncoding.EncodeToString(mac.Sum(nil)) } func setSession(w http.ResponseWriter, username string) { s := fmt.Sprintf("%s|%d", username, time.Now().Add(24*time.Hour).Unix()) sig := sign([]byte(s)) value := base64.StdEncoding.EncodeToString([]byte(s)) + "." + sig http.SetCookie(w, &http.Cookie{Name: cookieName, Value: value, Path: "/", HttpOnly: true, Secure: false, SameSite: http.SameSiteLaxMode}) } func clearSession(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{Name: cookieName, Value: "", Path: "/", Expires: time.Unix(0, 0)}) } func getSession(r *http.Request) (string, bool) { c, err := r.Cookie(cookieName) if err != nil { return "", false } parts := strings.SplitN(c.Value, ".", 2) if len(parts) != 2 { return "", false } payloadB, err := base64.StdEncoding.DecodeString(parts[0]) if err != nil { return "", false } if sign(payloadB) != parts[1] { return "", false } sp := strings.Split(string(payloadB), "|") if len(sp) != 2 { return "", false } exp, _ := strconv.ParseInt(sp[1], 10, 64) if time.Now().Unix() > exp { return "", false } return sp[0], true } func withAuth(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { u, ok := getSession(r) if !ok { http.Redirect(w, r, "/login", http.StatusFound) return } r = r.WithContext(context.WithValue(r.Context(), userKey, u)) h(w, r) } } func currentUser(r *http.Request) string { if v := r.Context().Value(userKey); v != nil { if s, ok := v.(string); ok { return s } } return "" } // ======= Templates ======= var baseTpl = `{{define "base"}} {{block "title" .}}Advocacy Watchlist{{end}}
Advocacy Watchlist
{{if .IsAdmin}}👥 Nutzer{{end}} 🪪 Audit 🔒 Passwort 👤 {{.User}}
{{template "content" .}}
{{end}}` var loginTpl = `{{define "login"}} Login · Advocacy Watchlist

Anmeldung

🌐 View public Entries
{{if .Msg}}
{{.Msg}}
{{end}} {{if .Setup}}
Erstbenutzer: {{.SetupUser}} / Passwort: {{.SetupPass}} (bitte nach dem Login ändern)
{{end}}
{{end}}` var dashboardTpl = `{{define "dashboard"}}{{template "base" .}}{{end}} {{define "content"}}

Neuen Benutzernamen melden

Einträge starten mit Status Gemeldet und müssen von einem anderen Nutzer bestätigt werden.

Audit-Log

Letzte 8 Ereignisse
{{range .RecentAudit}} {{else}} {{end}}
ZeitAktionDetails
{{.When}} {{.Action}} {{.Details}}
Noch keine Ereignisse

Gemeldete Benutzernamen

Gesamtes Audit
{{range .Reports}} {{else}} {{end}}
IDNameStatusAktionen
#{{.ID}} {{.Name}} {{if eq .Status "Gemeldet"}} Gemeldet {{else}} Bestätigt {{end}}
von {{.ReportedBy}} am {{.Created}}
{{if .Confirmed}}
bestätigt von {{.ConfirmedBy}} am {{.Confirmed}}
{{end}} {{ $rid := .ID }} {{/* Report-ID vor dem Reasons-Range sichern */}} {{if .Reasons}}
Gründe:
    {{range .Reasons}}
  • {{.Text}} — {{.AddedBy}} · {{.AddedAt}} {{if $.IsAdmin}}
    {{end}}
  • {{end}}
{{end}}
{{if eq .Status "Gemeldet"}} {{if ne $.User .ReportedBy}}
{{else}} ⏳ Wartet auf Bestätigung {{end}} {{else}} ✔ Bereits bestätigt {{end}} {{if $.IsAdmin}}
{{end}}
Keine Einträge
{{end}}` var publicTpl = `{{define "public_page"}} Advocacy Watchlist - Public
Advocacy Watchlist

Public list of confirmed entries

Only confirmed entries are displayed.
{{range .Rows}} {{else}} {{end}}
BenutzernameConfirmed on
{{.Name}} {{.Since}}
No matching entries
{{end}}` var auditTpl = `{{define "audit_page"}} Audit · Meldesystem
⬅ Zurück 👤 {{.User}}

Audit-Log (vollständig)

{{range .Rows}} {{else}} {{end}}
ZeitNutzerAktionDetailsIP
{{.When}} {{.Actor}} {{.Action}} {{.Details}} {{.IP}}
Noch keine Ereignisse
{{end}}` var usersTpl = `{{define "users_page"}} Nutzerverwaltung · Meldesystem

Nutzerverwaltung

⬅ Zurück 👤 {{.User}}
{{range .Users}} {{else}} {{end}}
NutzerRolleErstelltAktionen
{{.Username}} {{if .IsAdmin}}Admin{{else}}Nutzer{{end}} {{.Created}}
Keine Nutzer vorhanden
{{if .Msg}}
{{.Msg}}
{{end}}
{{end}}` var meTpl = `{{define "me_page"}} Passwort ändern · Meldesystem
⬅ Zurück
👤 {{.User}}

Passwort ändern

{{if .Msg}}
{{.Msg}}
{{end}}
{{end}}` // ======= Server ======= func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) // Geheimschlüssel laden/erzeugen if k := os.Getenv("APP_SECRET"); k != "" { secretKey = []byte(k) } else { secretKey = make([]byte, 32) if _, err := crand.Read(secretKey); err != nil { log.Fatal(err) } } if err := loadDB(); err != nil { log.Fatal(err) } setupUser, setupPass, setup := ensureAdmin() tpl = template.Must(template.New("base").Parse(baseTpl)) template.Must(tpl.New("dashboard_bundle").Parse(dashboardTpl)) template.Must(tpl.New("audit_bundle").Parse(auditTpl)) template.Must(tpl.New("users_bundle").Parse(usersTpl)) template.Must(tpl.New("me_bundle").Parse(meTpl)) template.Must(tpl.New("public_bundle").Parse(publicTpl)) http.HandleFunc("/", withAuth(handleIndex)) http.HandleFunc("/public", handlePublic) http.HandleFunc("/reasons/add", withAuth(handleReasonAdd)) http.HandleFunc("/reasons/delete", withAuth(handleReasonDelete)) http.HandleFunc("/report", withAuth(handleReport)) http.HandleFunc("/confirm", withAuth(handleConfirm)) http.HandleFunc("/delete", withAuth(handleDelete)) http.HandleFunc("/audit", withAuth(handleAudit)) http.HandleFunc("/users", withAuth(handleUsers)) http.HandleFunc("/users/create", withAuth(handleUserCreate)) http.HandleFunc("/users/resetpw", withAuth(handleUserResetPW)) http.HandleFunc("/users/toggleadmin", withAuth(handleUserToggleAdmin)) http.HandleFunc("/users/delete", withAuth(handleUserDelete)) http.HandleFunc("/me", withAuth(handleMe)) http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { t := template.Must(template.New("login").Parse(loginTpl)) msg := r.URL.Query().Get("msg") _ = t.ExecuteTemplate(w, "login", map[string]any{"Msg": msg, "Setup": setup, "SetupUser": setupUser, "SetupPass": setupPass}) return } if r.Method == http.MethodPost { username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") if authenticate(username, password) { setSession(w, username) appendAudit(username, "login", "Erfolg", r) http.Redirect(w, r, "/", http.StatusFound) return } appendAudit(username, "login", "Fehlgeschlagen", r) http.Redirect(w, r, "/login?msg=Ungültige%20Anmeldedaten", http.StatusFound) return } w.WriteHeader(http.StatusMethodNotAllowed) }) http.HandleFunc("/logout", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } u := currentUser(r) clearSession(w) appendAudit(u, "logout", "", r) http.Redirect(w, r, "/login?msg=Abgemeldet", http.StatusFound) }) addr := envOr("ADDR", ":8080") log.Printf("Server läuft auf %s", addr) log.Fatal(http.ListenAndServe(addr, nil)) } // ======= Admin/Users ======= func ensureAdmin() (string, string, bool) { mu.Lock() defer mu.Unlock() if len(db.Users) > 0 { return "", "", false } user := envOr("ADMIN_USER", "admin") pass := os.Getenv("ADMIN_PASS") if pass == "" { pass = randomPassword(12) } h := hashPassword(pass, newSalt()) db.Users = append(db.Users, User{Username: user, PasswordHash: h, IsAdmin: true, CreatedAt: time.Now().Unix()}) if err := saveDB(); err != nil { log.Fatal(err) } log.Printf("Erstbenutzer angelegt: %s / %s (bitte ändern)", user, pass) return user, pass, true } func authenticate(username, password string) bool { mu.Lock() defer mu.Unlock() for _, u := range db.Users { if u.Username == username && verifyPassword(password, u.PasswordHash) { return true } } return false } func isAdmin(username string) bool { mu.Lock() defer mu.Unlock() for _, u := range db.Users { if u.Username == username { return u.IsAdmin } } return false } // ======= Audit ======= func appendAudit(actor, action, details string, r *http.Request) { mu.Lock() defer mu.Unlock() entry := AuditEntry{Time: time.Now().Unix(), Actor: actor, Action: action, Details: details, IP: clientIP(r)} db.Audit = append([]AuditEntry{entry}, db.Audit...) if len(db.Audit) > 5000 { db.Audit = db.Audit[:5000] } _ = saveDB() } func clientIP(r *http.Request) string { if x := r.Header.Get("X-Forwarded-For"); x != "" { return strings.Split(x, ",")[0] } return strings.Split(r.RemoteAddr, ":")[0] } func recentAudit(n int) []map[string]any { mu.Lock() defer mu.Unlock() if n > len(db.Audit) { n = len(db.Audit) } out := make([]map[string]any, 0, n) for _, a := range db.Audit[:n] { out = append(out, map[string]any{ "When": time.Unix(a.Time, 0).Format("02.01.2006 15:04:05"), "Actor": a.Actor, "Action": a.Action, "Details": a.Details, "IP": a.IP, }) } return out } // ======= Benutzerverwaltung (Logik) ======= func findUser(username string) (int, *User) { for i := range db.Users { if db.Users[i].Username == username { return i, &db.Users[i] } } return -1, nil } func getUser(username string) (User, bool) { mu.Lock() defer mu.Unlock() for _, u := range db.Users { if u.Username == username { return u, true } } return User{}, false } func countAdmins() int { c := 0 for _, u := range db.Users { if u.IsAdmin { c++ } } return c } func createUser(username, password string, isAdmin bool) error { mu.Lock() defer mu.Unlock() if username == "" || password == "" { return errors.New("leer") } if _, u := findUser(username); u != nil { return errors.New("existiert bereits") } u := User{Username: username, PasswordHash: hashPassword(password, newSalt()), IsAdmin: isAdmin, CreatedAt: time.Now().Unix()} db.Users = append(db.Users, u) return saveDB() } func setPassword(username, newpw string) error { mu.Lock() defer mu.Unlock() idx, u := findUser(username) if u == nil { return errors.New("nicht gefunden") } db.Users[idx].PasswordHash = hashPassword(newpw, newSalt()) return saveDB() } func setAdmin(username string, admin bool) error { mu.Lock() defer mu.Unlock() idx, u := findUser(username) if u == nil { return errors.New("nicht gefunden") } if !admin && u.IsAdmin && countAdmins() == 1 { return errors.New("letzter Admin darf nicht entfernt werden") } db.Users[idx].IsAdmin = admin return saveDB() } func deleteUser(username, actor string) error { mu.Lock() defer mu.Unlock() idx, u := findUser(username) if u == nil { return errors.New("nicht gefunden") } if u.IsAdmin && countAdmins() == 1 { return errors.New("letzter Admin darf nicht gelöscht werden") } if username == actor { return errors.New("eigener Account kann nicht gelöscht werden") } db.Users = append(db.Users[:idx], db.Users[idx+1:]...) return saveDB() } // ======= HTTP-Handler ======= func handleIndex(w http.ResponseWriter, r *http.Request) { q := strings.TrimSpace(r.URL.Query().Get("q")) mu.Lock() reports := make([]Report, 0, len(db.Reports)) for _, rep := range db.Reports { if q == "" || strings.Contains(strings.ToLower(rep.Name), strings.ToLower(q)) { reports = append(reports, rep) } } mu.Unlock() rows := make([]map[string]any, 0, len(reports)) for _, rep := range reports { m := map[string]any{ "ID": rep.ID, "Name": template.HTMLEscapeString(rep.Name), "Status": rep.Status, "ReportedBy": rep.ReportedBy, "Created": time.Unix(rep.CreatedAt, 0).Format("02.01.2006 15:04"), } if rep.Status == StatusBestaetigt { m["Confirmed"] = time.Unix(rep.ConfirmedAt, 0).Format("02.01.2006 15:04") m["ConfirmedBy"] = rep.ConfirmedBy } if len(rep.Reasons) > 0 { rs := make([]map[string]any, 0, len(rep.Reasons)) for _, rsn := range rep.Reasons { rs = append(rs, map[string]any{ "ID": rsn.ID, "Text": template.HTMLEscapeString(rsn.Text), "AddedBy": rsn.AddedBy, "AddedAt": time.Unix(rsn.AddedAt, 0).Format("02.01.2006 15:04"), }) } m["Reasons"] = rs } rows = append(rows, m) } data := map[string]any{"User": currentUser(r), "IsAdmin": isAdmin(currentUser(r)), "Reports": rows, "Q": q, "RecentAudit": recentAudit(8), "Now": time.Now().Format("02.01.2006 15:04")} _ = tpl.ExecuteTemplate(w, "dashboard", data) } func handlePublic(w http.ResponseWriter, r *http.Request) { q := strings.TrimSpace(r.URL.Query().Get("q")) ql := strings.ToLower(q) mu.Lock() reports := make([]Report, 0, len(db.Reports)) for _, rep := range db.Reports { if rep.Status != StatusBestaetigt { continue // nur bestätigte öffentlich anzeigen } if q == "" || strings.Contains(strings.ToLower(rep.Name), ql) { reports = append(reports, rep) } } mu.Unlock() rows := make([]map[string]any, 0, len(reports)) for _, rep := range reports { rows = append(rows, map[string]any{ "Name": template.HTMLEscapeString(rep.Name), "Since": time.Unix(rep.ConfirmedAt, 0).Format("02.01.2006 15:04"), }) } _ = tpl.ExecuteTemplate(w, "public_page", map[string]any{ "Q": q, "Rows": rows, }) } func handleReport(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } name := strings.TrimSpace(r.FormValue("name")) reasonText := strings.TrimSpace(r.FormValue("reason")) if name == "" { http.Redirect(w, r, "/?msg=leer", http.StatusFound) return } user := currentUser(r) mu.Lock() for _, rep := range db.Reports { if strings.EqualFold(rep.Name, name) { mu.Unlock() appendAudit(user, "report_skip", fmt.Sprintf("Duplikat: %s", name), r) http.Redirect(w, r, "/?msg=exists", http.StatusFound) return } } id := db.NextID db.NextID++ rep := Report{ ID: id, Name: name, Status: StatusGemeldet, ReportedBy: user, CreatedAt: time.Now().Unix(), } if reasonText != "" { rid := db.NextRID db.NextRID++ rep.Reasons = []Reason{{ ID: rid, Text: reasonText, AddedBy: user, AddedAt: time.Now().Unix(), }} } db.Reports = append([]Report{rep}, db.Reports...) _ = saveDB() mu.Unlock() appendAudit(user, "report", fmt.Sprintf("ID %d · %s", rep.ID, rep.Name), r) http.Redirect(w, r, "/", http.StatusFound) } func handleReasonAdd(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } user := currentUser(r) rid, _ := strconv.ParseInt(r.FormValue("id"), 10, 64) text := strings.TrimSpace(r.FormValue("text")) if text == "" { http.Redirect(w, r, "/?msg=reason_empty", http.StatusFound) return } var ok bool mu.Lock() for i := range db.Reports { if db.Reports[i].ID == rid { // optional: Limit/Validierung if len(db.Reports[i].Reasons) > 50 { break } id := db.NextRID db.NextRID++ db.Reports[i].Reasons = append(db.Reports[i].Reasons, Reason{ ID: id, Text: text, AddedBy: user, AddedAt: time.Now().Unix(), }) _ = saveDB() ok = true break } } mu.Unlock() if ok { appendAudit(user, "reason_add", fmt.Sprintf("Report %d", rid), r) } else { appendAudit(user, "reason_add_failed", fmt.Sprintf("Report %d", rid), r) } http.Redirect(w, r, "/", http.StatusFound) } func handleReasonDelete(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } if !isAdmin(currentUser(r)) { w.WriteHeader(http.StatusForbidden) return } user := currentUser(r) rid, _ := strconv.ParseInt(r.FormValue("id"), 10, 64) // Report-ID rsid, _ := strconv.ParseInt(r.FormValue("rsid"), 10, 64) // Reason-ID var ok bool mu.Lock() for i := range db.Reports { if db.Reports[i].ID == rid { rs := db.Reports[i].Reasons for j := range rs { if rs[j].ID == rsid { db.Reports[i].Reasons = append(rs[:j], rs[j+1:]...) _ = saveDB() ok = true break } } break } } mu.Unlock() if ok { appendAudit(user, "reason_delete", fmt.Sprintf("Report %d, Reason %d", rid, rsid), r) } else { appendAudit(user, "reason_delete_failed", fmt.Sprintf("Report %d, Reason %d", rid, rsid), r) } http.Redirect(w, r, "/", http.StatusFound) } func handleConfirm(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } id, _ := strconv.ParseInt(r.FormValue("id"), 10, 64) user := currentUser(r) var did bool var name string mu.Lock() for i, rep := range db.Reports { if rep.ID == id { if rep.ReportedBy == user { mu.Unlock() appendAudit(user, "confirm_denied", fmt.Sprintf("ID %d · eigener Eintrag", id), r) http.Redirect(w, r, "/", http.StatusFound) return } if rep.Status == StatusBestaetigt { break } rep.Status = StatusBestaetigt rep.ConfirmedBy = user rep.ConfirmedAt = time.Now().Unix() name = rep.Name db.Reports[i] = rep _ = saveDB() did = true break } } mu.Unlock() if did { appendAudit(user, "confirm", fmt.Sprintf("ID %d · %s", id, name), r) } http.Redirect(w, r, "/", http.StatusFound) } func handleDelete(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } if !isAdmin(currentUser(r)) { w.WriteHeader(http.StatusForbidden) return } id, _ := strconv.ParseInt(r.FormValue("id"), 10, 64) user := currentUser(r) var name string var ok bool mu.Lock() for i, rep := range db.Reports { if rep.ID == id { name = rep.Name db.Reports = append(db.Reports[:i], db.Reports[i+1:]...) _ = saveDB() ok = true break } } mu.Unlock() if ok { appendAudit(user, "delete", fmt.Sprintf("ID %d · %s", id, name), r) } http.Redirect(w, r, "/", http.StatusFound) } func handleAudit(w http.ResponseWriter, r *http.Request) { rows := recentAudit(1000) data := map[string]any{"User": currentUser(r), "Rows": rows, "Now": time.Now().Format("02.01.2006 15:04"), "IsAdmin": isAdmin(currentUser(r))} _ = tpl.ExecuteTemplate(w, "audit_page", data) } func requireAdmin(w http.ResponseWriter, r *http.Request) bool { if !isAdmin(currentUser(r)) { w.WriteHeader(http.StatusForbidden) return false } return true } func handleUsers(w http.ResponseWriter, r *http.Request) { if !requireAdmin(w, r) { return } mu.Lock() rows := make([]map[string]any, 0, len(db.Users)) for _, u := range db.Users { rows = append(rows, map[string]any{"Username": u.Username, "IsAdmin": u.IsAdmin, "Created": time.Unix(u.CreatedAt, 0).Format("02.01.2006 15:04")}) } mu.Unlock() _ = tpl.ExecuteTemplate(w, "users_page", map[string]any{"User": currentUser(r), "Users": rows}) } func handleUserCreate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } if !requireAdmin(w, r) { return } username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") isAdmin := r.FormValue("is_admin") == "1" if err := createUser(username, password, isAdmin); err != nil { appendAudit(currentUser(r), "user_create_failed", err.Error(), r) } else { appendAudit(currentUser(r), "user_create", username, r) } http.Redirect(w, r, "/users", http.StatusFound) } func handleUserResetPW(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } if !requireAdmin(w, r) { return } username := r.FormValue("username") newpw := randomPassword(12) fmt.Println(newpw) if err := setPassword(username, newpw); err != nil { appendAudit(currentUser(r), "user_resetpw_failed", username+": "+err.Error(), r) } else { appendAudit(currentUser(r), "user_resetpw", username, r) } http.Redirect(w, r, "/users", http.StatusFound) } func handleUserToggleAdmin(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } if !requireAdmin(w, r) { return } username := r.FormValue("username") u, ok := getUser(username) if !ok { http.Redirect(w, r, "/users", http.StatusFound) return } if err := setAdmin(username, !u.IsAdmin); err != nil { appendAudit(currentUser(r), "user_toggle_admin_failed", username+": "+err.Error(), r) } else { appendAudit(currentUser(r), "user_toggle_admin", username, r) } http.Redirect(w, r, "/users", http.StatusFound) } func handleUserDelete(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } if !requireAdmin(w, r) { return } username := r.FormValue("username") if err := deleteUser(username, currentUser(r)); err != nil { appendAudit(currentUser(r), "user_delete_failed", username+": "+err.Error(), r) } else { appendAudit(currentUser(r), "user_delete", username, r) } http.Redirect(w, r, "/users", http.StatusFound) } func handleMe(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { _ = tpl.ExecuteTemplate(w, "me_page", map[string]any{"User": currentUser(r)}) return } if r.Method == http.MethodPost { old := r.FormValue("old") newp := r.FormValue("new") user := currentUser(r) mu.Lock() idx, u := findUser(user) if u == nil { mu.Unlock() w.WriteHeader(http.StatusUnauthorized) return } if !verifyPassword(old, u.PasswordHash) { mu.Unlock() appendAudit(user, "pw_change_failed", "falsches altes Passwort", r) _ = tpl.ExecuteTemplate(w, "me_page", map[string]any{"User": user, "Msg": "Altes Passwort falsch"}) return } db.Users[idx].PasswordHash = hashPassword(newp, newSalt()) _ = saveDB() mu.Unlock() appendAudit(user, "pw_change", "", r) _ = tpl.ExecuteTemplate(w, "me_page", map[string]any{"User": user, "Msg": "Gespeichert"}) return } w.WriteHeader(http.StatusMethodNotAllowed) } // ======= Utilities ======= func randomPassword(n int) string { letters := []rune("ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789@#%") b := make([]rune, n) s := mrand.New(mrand.NewSource(time.Now().UnixNano())) for i := range b { b[i] = letters[s.Intn(len(letters))] } return string(b) }