feat(PROJ-1): httpOnly Cookie, Auditor-Guard, Nutzer-Aktionen (C)
Backend: - Login setzt httpOnly SameSite=Strict Cookie (archivmail_session) - Logout löscht Cookie + blacklistet Token - authMiddleware: Cookie first, Bearer als Fallback (CLI kompatibel) Frontend: - api.ts: credentials: include statt localStorage/Bearer Token - updateUser(), deleteUser() hinzugefügt - useAuth: kein localStorage mehr, nur /api/auth/me requireRole: "admin" | "auditor" | undefined - Login-Seite: kein localStorage - Navbar: kein localStorage - Admin: Nutzer-Aktionen (Sperren/Freischalten, Löschen, Passwort-Reset) Löschen verhindert wenn letzter Admin (HTTP 409) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+41
-7
@@ -178,8 +178,17 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
Success: true,
|
||||
})
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sessionCookieName,
|
||||
Value: token,
|
||||
Path: "/",
|
||||
MaxAge: 8 * 3600,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
// Secure: true — enable when TLS is terminated at this server
|
||||
})
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"token": token,
|
||||
"user": map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"username": user.Username,
|
||||
@@ -206,11 +215,27 @@ func (s *Server) handleMe(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
token := extractBearerToken(r)
|
||||
if err := s.authMgr.Logout(token); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "logout failed")
|
||||
return
|
||||
// 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,
|
||||
})
|
||||
|
||||
sess := sessionFromCtx(r.Context())
|
||||
s.audlog.Log(audit.Entry{
|
||||
@@ -517,11 +542,20 @@ func (s *Server) handleAuditLog(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// --- middleware ---
|
||||
|
||||
const sessionCookieName = "archivmail_session"
|
||||
|
||||
func (s *Server) authMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
token := extractBearerToken(r)
|
||||
// Prefer httpOnly cookie; fall back to Bearer token for CLI/API clients.
|
||||
token := ""
|
||||
if c, err := r.Cookie(sessionCookieName); err == nil {
|
||||
token = c.Value
|
||||
}
|
||||
if token == "" {
|
||||
writeError(w, http.StatusUnauthorized, "missing authorization header")
|
||||
token = extractBearerToken(r)
|
||||
}
|
||||
if token == "" {
|
||||
writeError(w, http.StatusUnauthorized, "missing authorization")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user