// backend/user_log.go package main import ( "context" "encoding/json" "net" "net/http" "strings" ) type UserLogLevel string const ( UserLogLevelInfo UserLogLevel = "info" UserLogLevelWarning UserLogLevel = "warning" UserLogLevelError UserLogLevel = "error" ) const ( maxUserLogEntriesPerUser = 500 maxAnonymousUserLogEntries = 1000 ) type UserLogEntry struct { User *User UserID string Request *http.Request Level UserLogLevel Category string Action string Message string Error error Details LogFields } func (s *Server) logUserEvent(ctx context.Context, entry UserLogEntry) { if entry.Level == "" { entry.Level = UserLogLevelInfo } userID := any(nil) userIDForPrune := "" userSnapshot := map[string]any{} if entry.User != nil { userID = entry.User.ID userIDForPrune = entry.User.ID userSnapshot = buildUserLogSnapshot(*entry.User) } else if strings.TrimSpace(entry.UserID) != "" { userID = strings.TrimSpace(entry.UserID) userIDForPrune = strings.TrimSpace(entry.UserID) } requestSnapshot := map[string]any{} if entry.Request != nil { requestSnapshot = buildRequestLogSnapshot(entry.Request) } details := mergeLogFields(appErrorFields(entry.Error), entry.Details) errorMessage := "" if entry.Error != nil { errorMessage = entry.Error.Error() } userSnapshotJSON, err := json.Marshal(userSnapshot) if err != nil { logError("User-Log konnte nicht serialisiert werden", err, nil) return } requestSnapshotJSON, err := json.Marshal(requestSnapshot) if err != nil { logError("Request-Log konnte nicht serialisiert werden", err, nil) return } detailsJSON, err := json.Marshal(details) if err != nil { logError("User-Log-Details konnten nicht serialisiert werden", err, nil) return } _, err = s.db.Exec( ctx, ` INSERT INTO user_logs ( user_id, level, category, action, message, error, user_snapshot, request, details ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) `, userID, string(entry.Level), entry.Category, entry.Action, entry.Message, errorMessage, userSnapshotJSON, requestSnapshotJSON, detailsJSON, ) if err != nil { logError("User-Log konnte nicht gespeichert werden", err, LogFields{ "category": entry.Category, "action": entry.Action, }) return } if userIDForPrune != "" { s.pruneUserLogsByUserID(ctx, userIDForPrune) return } s.pruneAnonymousUserLogs(ctx) } // deleteUserLogs entfernt sämtliche Protokolleinträge eines Users. Wird nach // einem erfolgreichen Login aufgerufen, damit das Protokoll danach leer ist. func (s *Server) deleteUserLogs(ctx context.Context, userID string) { userID = strings.TrimSpace(userID) if userID == "" { return } _, err := s.db.Exec( ctx, `DELETE FROM user_logs WHERE user_id = $1`, userID, ) if err != nil { logError("User-Logs konnten beim Login nicht gelöscht werden", err, LogFields{ "userId": userID, }) } } func (s *Server) pruneUserLogs(ctx context.Context, user *User) { if user == nil || user.ID == "" { return } s.pruneUserLogsByUserID(ctx, user.ID) } func (s *Server) pruneUserLogsByUserID(ctx context.Context, userID string) { userID = strings.TrimSpace(userID) if userID == "" { return } _, err := s.db.Exec( ctx, ` DELETE FROM user_logs WHERE user_id = $1 AND id NOT IN ( SELECT id FROM user_logs WHERE user_id = $1 ORDER BY created_at DESC, id DESC LIMIT $2 ) `, userID, maxUserLogEntriesPerUser, ) if err != nil { logError("Alte User-Logs konnten nicht gelöscht werden", err, LogFields{ "userId": userID, "limit": maxUserLogEntriesPerUser, }) } } func (s *Server) pruneAnonymousUserLogs(ctx context.Context) { _, err := s.db.Exec( ctx, ` DELETE FROM user_logs WHERE user_id IS NULL AND id NOT IN ( SELECT id FROM user_logs WHERE user_id IS NULL ORDER BY created_at DESC, id DESC LIMIT $1 ) `, maxAnonymousUserLogEntries, ) if err != nil { logError("Alte anonyme User-Logs konnten nicht gelöscht werden", err, LogFields{ "limit": maxAnonymousUserLogEntries, }) } } func (s *Server) logUserError( r *http.Request, user User, category string, action string, message string, err error, details LogFields, ) { s.logUserEvent(r.Context(), UserLogEntry{ User: &user, Request: r, Level: UserLogLevelError, Category: category, Action: action, Message: message, Error: err, Details: details, }) } func (s *Server) logAndWriteUserError( w http.ResponseWriter, r *http.Request, user User, statusCode int, category string, action string, publicMessage string, err error, details LogFields, ) { s.logUserError( r, user, category, action, publicMessage, err, details, ) writeError(w, statusCode, publicMessage) } func buildUserLogSnapshot(user User) map[string]any { return map[string]any{ "id": user.ID, "username": user.Username, "displayName": user.DisplayName, "email": user.Email, "avatar": user.Avatar, "unit": user.Unit, "group": user.Group, "rights": user.Rights, "teams": user.Teams, } } func buildRequestLogSnapshot(r *http.Request) map[string]any { if r == nil { return map[string]any{} } return map[string]any{ "method": r.Method, "path": r.URL.Path, "query": r.URL.RawQuery, "ipAddress": requestIPAddress(r), "remoteAddr": r.RemoteAddr, "userAgent": r.UserAgent(), "referer": r.Referer(), } } func requestIPAddress(r *http.Request) string { for _, headerName := range []string{ "X-Forwarded-For", "X-Real-IP", "CF-Connecting-IP", } { headerValue := strings.TrimSpace(r.Header.Get(headerName)) if headerValue == "" { continue } ipAddress := strings.TrimSpace(strings.Split(headerValue, ",")[0]) if ipAddress != "" { return ipAddress } } host, _, err := net.SplitHostPort(r.RemoteAddr) if err == nil { return host } return r.RemoteAddr } func (s *Server) logRequestEvent( r *http.Request, level UserLogLevel, category string, action string, message string, err error, details LogFields, ) { var user *User userID := "" if currentUser, ok := userFromContext(r.Context()); ok { user = ¤tUser } else if currentUserID, ok := s.currentUserIDFromRequest(r); ok { userID = currentUserID } s.logUserEvent(r.Context(), UserLogEntry{ User: user, UserID: userID, Request: r, Level: level, Category: category, Action: action, Message: message, Error: err, Details: details, }) } func (s *Server) logRequestInfo( r *http.Request, category string, action string, message string, details LogFields, ) { s.logRequestEvent( r, UserLogLevelInfo, category, action, message, nil, details, ) } func (s *Server) logRequestWarning( r *http.Request, category string, action string, message string, err error, details LogFields, ) { s.logRequestEvent( r, UserLogLevelWarning, category, action, message, err, details, ) } func (s *Server) logRequestError( r *http.Request, category string, action string, message string, err error, details LogFields, ) { s.logRequestEvent( r, UserLogLevelError, category, action, message, err, details, ) } func (s *Server) logAndWriteSettingsError( w http.ResponseWriter, r *http.Request, statusCode int, category string, action string, publicMessage string, err error, details LogFields, ) { s.logRequestError( r, category, action, publicMessage, err, details, ) writeSettingsError(w, statusCode, publicMessage) }