126 lines
3.6 KiB
Go
126 lines
3.6 KiB
Go
package audit
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
// Handler exposes the audit log read API.
|
|
type Handler struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// NewHandler creates a new audit log handler.
|
|
func NewHandler(db *sql.DB) *Handler {
|
|
return &Handler{db: db}
|
|
}
|
|
|
|
// Entry represents a single audit log entry.
|
|
type Entry struct {
|
|
ID string `json:"id"`
|
|
ServerID string `json:"server_id"`
|
|
UserID *string `json:"user_id"`
|
|
Username *string `json:"username"`
|
|
ActionType string `json:"action_type"`
|
|
TargetType *string `json:"target_type"`
|
|
TargetID *string `json:"target_id"`
|
|
Reason *string `json:"reason"`
|
|
Changes json.RawMessage `json:"changes"`
|
|
CreatedAt string `json:"created_at"`
|
|
}
|
|
|
|
// List handles GET /servers/{serverID}/audit-log.
|
|
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
|
|
serverID := chi.URLParam(r, "serverID")
|
|
userID, ok := middleware.UserIDFromContext(r.Context())
|
|
if !ok {
|
|
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// ponytail: membership check keeps it simple; MANAGE_SERVER permission is the real gate.
|
|
var isMember bool
|
|
err := h.db.QueryRowContext(r.Context(), `SELECT EXISTS(SELECT 1 FROM members WHERE server_id = $1 AND user_id = $2)`, serverID, userID).Scan(&isMember)
|
|
if err != nil || !isMember {
|
|
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
limitStr := r.URL.Query().Get("limit")
|
|
limit := 50
|
|
if limitStr != "" {
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
|
limit = l
|
|
}
|
|
}
|
|
|
|
actionType := r.URL.Query().Get("action_type")
|
|
before := r.URL.Query().Get("before")
|
|
|
|
args := []interface{}{serverID}
|
|
query := `
|
|
SELECT a.id, a.server_id, a.user_id, u.username, a.action_type, a.target_type, a.target_id, a.reason, a.changes, a.created_at::text
|
|
FROM audit_log a
|
|
LEFT JOIN users u ON u.id = a.user_id
|
|
WHERE a.server_id = $1
|
|
`
|
|
if actionType != "" {
|
|
args = append(args, actionType)
|
|
query += ` AND a.action_type = $` + strconv.Itoa(len(args))
|
|
}
|
|
if before != "" {
|
|
args = append(args, before)
|
|
query += ` AND a.created_at < (SELECT created_at FROM audit_log WHERE id = $` + strconv.Itoa(len(args)) + `)`
|
|
}
|
|
query += ` ORDER BY a.created_at DESC LIMIT $` + strconv.Itoa(len(args)+1)
|
|
args = append(args, limit)
|
|
|
|
rows, err := h.db.QueryContext(r.Context(), query, args...)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
entries := make([]Entry, 0)
|
|
for rows.Next() {
|
|
var e Entry
|
|
var userIDStr, username, targetType, targetID, reason sql.NullString
|
|
err := rows.Scan(&e.ID, &e.ServerID, &userIDStr, &username, &e.ActionType, &targetType, &targetID, &reason, &e.Changes, &e.CreatedAt)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if userIDStr.Valid {
|
|
e.UserID = &userIDStr.String
|
|
}
|
|
if username.Valid {
|
|
e.Username = &username.String
|
|
}
|
|
if targetType.Valid {
|
|
e.TargetType = &targetType.String
|
|
}
|
|
if targetID.Valid {
|
|
e.TargetID = &targetID.String
|
|
}
|
|
if reason.Valid {
|
|
e.Reason = &reason.String
|
|
}
|
|
entries = append(entries, e)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(entries)
|
|
}
|
|
|
|
// Cleanup removes audit log entries older than 90 days.
|
|
func (h *Handler) Cleanup(ctx context.Context) error {
|
|
_, err := h.db.ExecContext(ctx, `DELETE FROM audit_log WHERE created_at < NOW() - INTERVAL '90 days'`)
|
|
return err
|
|
}
|