package backend import ( "context" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "fmt" "html" "net/http" "strings" "time" ) type contextKey string const ( sessionCookieName = "mail_graveyard_session" currentUserKey = contextKey("currentUser") roleUser = "user" roleManager = "verwalter" roleAdmin = "admin" passwordHashIterations = 120000 defaultSessionValidDays = 7 ) // InitAuth bereitet die Session-/Login-Verwaltung vor. // // Sicherheitslage: Dieses Tool haelt IMAP-Zugangsdaten fremder Postfaecher und // kann per SMTP Mails verschicken. Es darf NIE offen im Netz stehen. // - Default: bind 127.0.0.1 (nur lokal), Login mit admin_user/admin_pass. // - Wenn remote erreichbar (hinter Caddy): HTTPS und restriktive Caddy-Regeln // beibehalten. Rollen prueft die App serverseitig. func InitAuth() error { if err := CleanupSessions(); err != nil { return err } count, err := CountAppUsers() if err != nil { return err } if count > 0 { return nil } if strings.TrimSpace(Cfg.AdminUser) == "" || Cfg.AdminPass == "" { return fmt.Errorf("admin_user/admin_pass fehlen fuer initialen Login-Benutzer") } hash, err := HashPassword(Cfg.AdminPass) if err != nil { return err } return SaveAppUser(strings.TrimSpace(Cfg.AdminUser), "Admin", hash, roleAdmin, true) } // AuthMiddleware schuetzt alle Routen ausser /login und /static. func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.HasPrefix(r.URL.Path, "/static/") || r.URL.Path == "/login" { next.ServeHTTP(w, r) return } cookie, err := r.Cookie(sessionCookieName) if err != nil || strings.TrimSpace(cookie.Value) == "" { redirectToLogin(w, r) return } user, err := SessionUser(cookie.Value) if err != nil { clearSessionCookie(w) redirectToLogin(w, r) return } next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), currentUserKey, user))) }) } func CurrentUser(r *http.Request) AppUser { if u, ok := r.Context().Value(currentUserKey).(AppUser); ok { return u } return AppUser{} } func CanManageAccounts(r *http.Request) bool { return hasRole(CurrentUser(r), roleManager) } func CanManageUsers(r *http.Request) bool { return hasRole(CurrentUser(r), roleManager) } func IsAdmin(r *http.Request) bool { return CurrentUser(r).Role == roleAdmin } func requireManager(w http.ResponseWriter, r *http.Request) bool { if CanManageAccounts(r) { return true } http.Error(w, "Keine Berechtigung.", http.StatusForbidden) return false } func requireAdmin(w http.ResponseWriter, r *http.Request) bool { if IsAdmin(r) { return true } http.Error(w, "Nur Admins duerfen diese Aktion ausfuehren.", http.StatusForbidden) return false } func hasRole(user AppUser, needed string) bool { rank := map[string]int{roleUser: 1, roleManager: 2, roleAdmin: 3} return rank[user.Role] >= rank[needed] } func loginHandler(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: renderLoginPage(w, r.URL.Query().Get("err")) case http.MethodPost: if err := r.ParseForm(); err != nil { renderLoginPage(w, err.Error()) return } username := strings.TrimSpace(r.FormValue("username")) password := r.FormValue("password") user, err := GetAppUser(username) if err != nil || !user.Active || !CheckPassword(password, user.PasswordHash) { renderLoginPage(w, "Login fehlgeschlagen.") return } token, err := randomToken(32) if err != nil { renderLoginPage(w, err.Error()) return } expires := time.Now().Add(defaultSessionValidDays * 24 * time.Hour) if err := CreateSession(token, user.ID, expires.UTC().Format("2006-01-02 15:04:05")); err != nil { renderLoginPage(w, err.Error()) return } http.SetCookie(w, &http.Cookie{ Name: sessionCookieName, Value: token, Path: "/", HttpOnly: true, SameSite: http.SameSiteLaxMode, Expires: expires, MaxAge: int(time.Until(expires).Seconds()), }) http.Redirect(w, r, "/view", http.StatusSeeOther) default: http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } } func logoutHandler(w http.ResponseWriter, r *http.Request) { if cookie, err := r.Cookie(sessionCookieName); err == nil { _ = DeleteSession(cookie.Value) } clearSessionCookie(w) http.Redirect(w, r, "/login", http.StatusSeeOther) } func renderLoginPage(w http.ResponseWriter, errMsg string) { w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, `