422 lines
7.5 KiB
Go
422 lines
7.5 KiB
Go
// 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)
|
|
}
|