refactor: server.go in separate Handler-Dateien aufgeteilt
server.go (2357 -> 391 Zeilen) enthaelt nur noch Server-Struct, Konstruktor, Router, Middleware und Hilfsfunktionen. Neue Dateien: - auth_handlers.go: Login, Logout, Me - search_handlers.go: Suche, Mail-Anzeige, Anhaenge, Raw-Download - admin_handlers.go: User-CRUD, SMTP/Storage-Stats, Services, Security - import_handlers.go: IMAP + POP3 Account-Verwaltung und Import - dashboard_handlers.go: System-Stats, Audit-Log Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/archivmail/internal/audit"
|
||||
)
|
||||
|
||||
const (
|
||||
loginMaxFailures = 5
|
||||
loginWindow = 15 * time.Minute
|
||||
)
|
||||
|
||||
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
// Rate-limiting: block after too many recent failures
|
||||
failures, err := s.users.CountRecentFailures(req.Username, loginWindow)
|
||||
if err == nil && failures >= loginMaxFailures {
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: audit.EventLogin,
|
||||
Username: req.Username,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Success: false,
|
||||
Detail: "rate limited",
|
||||
})
|
||||
writeError(w, http.StatusTooManyRequests, "too many failed login attempts, try again later")
|
||||
return
|
||||
}
|
||||
|
||||
token, user, totpRequired, err := s.authMgr.Login(req.Username, req.Password)
|
||||
if err != nil {
|
||||
_ = s.users.RecordLoginAttempt(req.Username, s.remoteIP(r))
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: audit.EventLogin,
|
||||
Username: req.Username,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Success: false,
|
||||
Detail: classifyLoginError(err),
|
||||
})
|
||||
writeError(w, http.StatusUnauthorized, "invalid credentials")
|
||||
return
|
||||
}
|
||||
|
||||
// PROJ-24: If TOTP is enabled, return a pending token instead of a full session.
|
||||
if totpRequired {
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: audit.EventLogin,
|
||||
Username: user.Username,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Success: true,
|
||||
Detail: "totp_pending",
|
||||
})
|
||||
writeJSON(w, http.StatusAccepted, map[string]interface{}{
|
||||
"totp_required": true,
|
||||
"session_token": token,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
_ = s.users.UpdateLastLogin(user.ID)
|
||||
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: audit.EventLogin,
|
||||
Username: user.Username,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Success: true,
|
||||
})
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
MaxAge: 8 * 3600,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: s.cfg.SecureCookies,
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"user": map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) {
|
||||
sess := sessionFromCtx(r.Context())
|
||||
|
||||
user, err := s.users.GetByUsername(sess.Username)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "user lookup failed")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
"role": user.Role,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
// Read token from cookie first, then Bearer header
|
||||
token := ""
|
||||
if c, err := r.Cookie(sessionCookieName); err == nil {
|
||||
token = c.Value
|
||||
}
|
||||
if token == "" {
|
||||
token = extractBearerToken(r)
|
||||
}
|
||||
if token != "" {
|
||||
_ = s.authMgr.Logout(token)
|
||||
}
|
||||
|
||||
// Clear the session cookie
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Secure: s.cfg.SecureCookies,
|
||||
})
|
||||
|
||||
sess := sessionFromCtx(r.Context())
|
||||
s.audlog.Log(audit.Entry{
|
||||
EventType: audit.EventLogout,
|
||||
Username: sess.Username,
|
||||
IPAddress: s.remoteIP(r),
|
||||
Success: true,
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
|
||||
}
|
||||
Reference in New Issue
Block a user