feat(phase2): bulk delete + audit log; update roadmap/parity docs
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user