diff --git a/data.json b/data.json index fbc288e..4429b66 100644 --- a/data.json +++ b/data.json @@ -2,86 +2,339 @@ "users": [ { "username": "admin", - "password_hash": "/VpEC5P5RjBe9KaKoGka3IAyqEImtOH4DX9pa4fGq0s=:KSkNZvZGhywj3xKzn1T39w==", + "password_hash": "hBaSLUNhYTfD8t5QgMBAyRgb/IsdpOzG70XzIaPWPik=:LeiXx83tVF0Vb08KjNwT/A==", "is_admin": true, - "created_at": 1759575291 + "created_at": 1759592823 + }, + { + "username": "jbergner", + "password_hash": "xO0T3HChR2wCqr2/EdGBh5DImelVl1WQZ7pji+MFW84=:yhjVxufYiqdzq0XQZBcRtQ==", + "is_admin": false, + "created_at": 1759593689 } ], "reports": [ { - "id": 1, + "id": 3, "name": "B1tK1ll3r", - "status": "Gemeldet", + "status": "Bestätigt", "reported_by": "admin", - "confirmed_by": "", - "created_at": 1759580762, - "confirmed_at": 0 + "confirmed_by": "jbergner", + "created_at": 1759593661, + "confirmed_at": 1759593901, + "reasons": [ + { + "id": 4, + "text": "Trottel", + "added_by": "admin", + "added_at": 1759593661 + } + ] } ], "audit": [ { - "time": 1759580868, - "actor": "admin", + "time": 1759593901, + "actor": "jbergner", + "action": "confirm", + "details": "ID 3 · B1tK1ll3r", + "ip": "127.0.0.1" + }, + { + "time": 1759593892, + "actor": "jbergner", + "action": "pw_change", + "details": "", + "ip": "127.0.0.1" + }, + { + "time": 1759593884, + "actor": "jbergner", "action": "login", "details": "Erfolg", "ip": "127.0.0.1" }, { - "time": 1759580762, - "actor": "admin", - "action": "report", - "details": "ID 1 · B1tK1ll3r", - "ip": "127.0.0.1" - }, - { - "time": 1759580750, - "actor": "admin", - "action": "login", - "details": "Erfolg", - "ip": "127.0.0.1" - }, - { - "time": 1759580355, - "actor": "admin", - "action": "login", - "details": "Erfolg", - "ip": "127.0.0.1" - }, - { - "time": 1759580347, - "actor": "admin", - "action": "login", - "details": "Fehlgeschlagen", - "ip": "127.0.0.1" - }, - { - "time": 1759580244, - "actor": "admin", - "action": "login", - "details": "Erfolg", - "ip": "127.0.0.1" - }, - { - "time": 1759579880, - "actor": "admin", - "action": "login", - "details": "Erfolg", - "ip": "127.0.0.1" - }, - { - "time": 1759579770, + "time": 1759593880, "actor": "", "action": "logout", "details": "", "ip": "127.0.0.1" }, { - "time": 1759579675, + "time": 1759593872, + "actor": "admin", + "action": "user_resetpw", + "details": "jbergner", + "ip": "127.0.0.1" + }, + { + "time": 1759593867, + "actor": "admin", + "action": "login", + "details": "Erfolg", + "ip": "127.0.0.1" + }, + { + "time": 1759593779, + "actor": "jbergner", + "action": "login", + "details": "Fehlgeschlagen", + "ip": "127.0.0.1" + }, + { + "time": 1759593771, + "actor": "jbergner", + "action": "login", + "details": "Fehlgeschlagen", + "ip": "127.0.0.1" + }, + { + "time": 1759593765, + "actor": "", + "action": "logout", + "details": "", + "ip": "127.0.0.1" + }, + { + "time": 1759593752, + "actor": "admin", + "action": "user_resetpw", + "details": "jbergner", + "ip": "127.0.0.1" + }, + { + "time": 1759593745, + "actor": "admin", + "action": "login", + "details": "Erfolg", + "ip": "127.0.0.1" + }, + { + "time": 1759593739, + "actor": "jbergner", + "action": "login", + "details": "Fehlgeschlagen", + "ip": "127.0.0.1" + }, + { + "time": 1759593734, + "actor": "", + "action": "logout", + "details": "", + "ip": "127.0.0.1" + }, + { + "time": 1759593715, + "actor": "admin", + "action": "user_resetpw", + "details": "jbergner", + "ip": "127.0.0.1" + }, + { + "time": 1759593710, + "actor": "admin", + "action": "login", + "details": "Erfolg", + "ip": "127.0.0.1" + }, + { + "time": 1759593702, + "actor": "jbergner", + "action": "login", + "details": "Fehlgeschlagen", + "ip": "127.0.0.1" + }, + { + "time": 1759593697, + "actor": "jbergner", + "action": "login", + "details": "Fehlgeschlagen", + "ip": "127.0.0.1" + }, + { + "time": 1759593692, + "actor": "", + "action": "logout", + "details": "", + "ip": "127.0.0.1" + }, + { + "time": 1759593689, + "actor": "admin", + "action": "user_create", + "details": "jbergner", + "ip": "127.0.0.1" + }, + { + "time": 1759593682, + "actor": "admin", + "action": "login", + "details": "Erfolg", + "ip": "127.0.0.1" + }, + { + "time": 1759593677, + "actor": "jbergner", + "action": "login", + "details": "Fehlgeschlagen", + "ip": "127.0.0.1" + }, + { + "time": 1759593672, + "actor": "jbergner", + "action": "login", + "details": "Fehlgeschlagen", + "ip": "127.0.0.1" + }, + { + "time": 1759593668, + "actor": "", + "action": "logout", + "details": "", + "ip": "127.0.0.1" + }, + { + "time": 1759593661, + "actor": "admin", + "action": "report", + "details": "ID 3 · B1tK1ll3r", + "ip": "127.0.0.1" + }, + { + "time": 1759593652, + "actor": "admin", + "action": "login", + "details": "Erfolg", + "ip": "127.0.0.1" + }, + { + "time": 1759593441, + "actor": "admin", + "action": "login", + "details": "Erfolg", + "ip": "127.0.0.1" + }, + { + "time": 1759593436, + "actor": "", + "action": "logout", + "details": "", + "ip": "127.0.0.1" + }, + { + "time": 1759593433, + "actor": "admin", + "action": "pw_change", + "details": "", + "ip": "127.0.0.1" + }, + { + "time": 1759593403, + "actor": "admin", + "action": "delete", + "details": "ID 1 · B1tK1ll3r", + "ip": "127.0.0.1" + }, + { + "time": 1759593401, + "actor": "admin", + "action": "delete", + "details": "ID 2 · Jan Bergner", + "ip": "127.0.0.1" + }, + { + "time": 1759593393, + "actor": "admin", + "action": "reason_delete", + "details": "Report 2, Reason 3", + "ip": "127.0.0.1" + }, + { + "time": 1759593388, + "actor": "admin", + "action": "report", + "details": "ID 2 · Jan Bergner", + "ip": "127.0.0.1" + }, + { + "time": 1759593380, + "actor": "admin", + "action": "login", + "details": "Erfolg", + "ip": "127.0.0.1" + }, + { + "time": 1759593252, + "actor": "admin", + "action": "reason_delete", + "details": "Report 1, Reason 1", + "ip": "127.0.0.1" + }, + { + "time": 1759593250, + "actor": "admin", + "action": "reason_delete", + "details": "Report 1, Reason 2", + "ip": "127.0.0.1" + }, + { + "time": 1759593246, + "actor": "admin", + "action": "login", + "details": "Erfolg", + "ip": "127.0.0.1" + }, + { + "time": 1759592953, + "actor": "admin", + "action": "reason_delete_failed", + "details": "Report 0, Reason 2", + "ip": "127.0.0.1" + }, + { + "time": 1759592948, + "actor": "admin", + "action": "reason_delete_failed", + "details": "Report 0, Reason 1", + "ip": "127.0.0.1" + }, + { + "time": 1759592946, + "actor": "admin", + "action": "reason_add", + "details": "Report 1", + "ip": "127.0.0.1" + }, + { + "time": 1759592871, + "actor": "admin", + "action": "reason_delete_failed", + "details": "Report 0, Reason 1", + "ip": "127.0.0.1" + }, + { + "time": 1759592869, + "actor": "admin", + "action": "reason_add", + "details": "Report 1", + "ip": "127.0.0.1" + }, + { + "time": 1759592866, + "actor": "admin", + "action": "report", + "details": "ID 1 · B1tK1ll3r", + "ip": "127.0.0.1" + }, + { + "time": 1759592833, "actor": "admin", "action": "login", "details": "Erfolg", "ip": "127.0.0.1" } ], - "next_id": 2 + "next_id": 4, + "next_rid": 5 } diff --git a/main.go b/main.go index 6ae3f1a..06cf13e 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,12 @@ // main.go // Go 1.21+ -// Einfache Webanwendung mit Login, Persistenz (JSON-Datei), Audit-Log, -// und UI ohne externe Verweise. Nutzer können "Benutzernamen" melden, -// Einträge starten mit Status "Gemeldet" und können von einem anderen -// Nutzer bestätigt werden. Alles wird audit-geeignet protokolliert. +// 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. // -// Hinweise: Für Produktion TLS, CSRF, bcrypt/argon2id etc. ergänzen. +// PRODUKTIONSHINWEISE: HTTPS erzwingen (Secure-Cookie), CSRF-Schutz, +// bcrypt/argon2id statt Demo-Hash, Rate Limiting, echte DB, Passwort-Richtlinien. package main @@ -16,6 +17,7 @@ import ( "crypto/sha256" "encoding/base64" "encoding/json" + "errors" "fmt" "html/template" "log" @@ -46,13 +48,21 @@ const ( ) 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"` + 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 { @@ -68,6 +78,7 @@ type DB struct { Reports []Report `json:"reports"` Audit []AuditEntry `json:"audit"` NextID int64 `json:"next_id"` + NextRID int64 `json:"next_rid"` } // ======= Globale Variablen ======= @@ -75,7 +86,7 @@ type DB struct { var ( dataPath = envOr("DATA_FILE", "data.json") cookieName = envOr("COOKIE_NAME", "session") - secretKey []byte // für HMAC der Session-Cookies + secretKey []byte // HMAC für Session-Cookies mu sync.Mutex db DB @@ -83,7 +94,7 @@ var ( tpl *template.Template ) -// ======= Hilfsfunktionen ======= +// ======= Helpers: Datei + Env ======= func envOr(k, def string) string { if v := os.Getenv(k); v != "" { @@ -95,18 +106,17 @@ func envOr(k, def string) string { func loadDB() error { mu.Lock() defer mu.Unlock() - f, err := os.Open(dataPath) if err != nil { if os.IsNotExist(err) { - db = DB{NextID: 1} + db = DB{NextID: 1, NextRID: 1} return saveDB() } return err } defer f.Close() - decoder := json.NewDecoder(f) - return decoder.Decode(&db) + dec := json.NewDecoder(f) + return dec.Decode(&db) } func saveDB() error { @@ -125,14 +135,13 @@ func saveDB() error { return os.Rename(f.Name(), dataPath) } -// ======= Pseudo-PBKDF (nur Demo!) ======= -// Für Produktion: bcrypt/argon2id verwenden. +// ======= Passwort-Hash (Demo!) ======= func hashPassword(pw string, salt []byte) string { h := sha256.New() h.Write(salt) h.Write([]byte(pw)) - for i := 0; i < 100000; i++ { // 100k Runden + for i := 0; i < 100_000; i++ { h.Write([]byte{byte(i)}) } return base64.StdEncoding.EncodeToString(h.Sum(nil)) + ":" + base64.StdEncoding.EncodeToString(salt) @@ -150,9 +159,13 @@ func verifyPassword(pw, stored string) bool { return hashPassword(pw, salt) == stored } -func newSalt() []byte { b := make([]byte, 16); crand.Read(b); return b } +func newSalt() []byte { b := make([]byte, 16); _, _ = crand.Read(b); return b } -// ======= Sessions (signiertes Cookie) ======= +// ======= Sessions ======= + +type ctxKey string + +var userKey ctxKey = "user" type Session struct { Username string @@ -169,14 +182,7 @@ 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, // In Produktion: true + HTTPS - SameSite: http.SameSiteLaxMode, - }) + http.SetCookie(w, &http.Cookie{Name: cookieName, Value: value, Path: "/", HttpOnly: true, Secure: false, SameSite: http.SameSiteLaxMode}) } func clearSession(w http.ResponseWriter) { @@ -199,8 +205,7 @@ func getSession(r *http.Request) (string, bool) { if sign(payloadB) != parts[1] { return "", false } - payload := string(payloadB) - sp := strings.Split(payload, "|") + sp := strings.Split(string(payloadB), "|") if len(sp) != 2 { return "", false } @@ -211,12 +216,6 @@ func getSession(r *http.Request) (string, bool) { return sp[0], true } -// ======= Middleware ======= - -type ctxKey string - -var userKey ctxKey = "user" - func withAuth(h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { u, ok := getSession(r) @@ -224,8 +223,7 @@ func withAuth(h http.HandlerFunc) http.HandlerFunc { http.Redirect(w, r, "/login", http.StatusFound) return } - ctx := context.WithValue(r.Context(), userKey, u) - r = r.WithContext(ctx) + r = r.WithContext(context.WithValue(r.Context(), userKey, u)) h(w, r) } } @@ -239,7 +237,7 @@ func currentUser(r *http.Request) string { return "" } -// ======= Template & Assets ======= +// ======= Templates ======= var baseTpl = `{{define "base"}} @@ -250,7 +248,7 @@ var baseTpl = `{{define "base"}}