Files
boc/aamos-admin-upgrade/backend/handlers/audit.go
T
Bernt 6de2455917 v1.2.0: Add Global Markets footer, translated to 9 languages
- Added GLOBAL_MARKETS_TITLE to all translation files
- Updated footer with 12 markets (4 active + 8 upcoming)
- Translated market section to: zh-cn, zh-tw, ja, ko, th, vi, id, ms, hi
- Built and deployed to production
- CloudFront invalidation: I3RTMXVFDJXWLG3SYX208OP1CC
2026-07-08 19:56:03 +00:00

170 lines
4.3 KiB
Go

package handlers
import (
"crypto/rand"
"database/sql"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
type AuditEntry struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Action string `json:"action"`
Resource string `json:"resource"`
IP string `json:"ip"`
Timestamp time.Time `json:"timestamp"`
Details string `json:"details"`
}
type AuditListResponse struct {
Data []AuditEntry `json:"data"`
Total int `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
// AuditHandler returns a handler for GET /api/v1/audit.
// Query params: page (default 1), limit (default 20, max 100),
// user (user_id filter), action, resource.
func AuditHandler(db *sql.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
q := r.URL.Query()
page, err := parsePositiveInt(q.Get("page"), 1)
if err != nil {
http.Error(w, "invalid page", http.StatusBadRequest)
return
}
limit, err := parsePositiveInt(q.Get("limit"), 20)
if err != nil {
http.Error(w, "invalid limit", http.StatusBadRequest)
return
}
if limit > 100 {
limit = 100
}
userFilter := strings.TrimSpace(q.Get("user"))
actionFilter := strings.TrimSpace(q.Get("action"))
resourceFilter := strings.TrimSpace(q.Get("resource"))
var conditions []string
var args []interface{}
idx := 1
if userFilter != "" {
conditions = append(conditions, fmt.Sprintf("user_id = $%d", idx))
args = append(args, userFilter)
idx++
}
if actionFilter != "" {
conditions = append(conditions, fmt.Sprintf("action = $%d", idx))
args = append(args, actionFilter)
idx++
}
if resourceFilter != "" {
conditions = append(conditions, fmt.Sprintf("resource = $%d", idx))
args = append(args, resourceFilter)
idx++
}
where := ""
if len(conditions) > 0 {
where = "WHERE " + strings.Join(conditions, " AND ")
}
var total int
if err := db.QueryRow(
fmt.Sprintf("SELECT COUNT(*) FROM audit_logs %s", where),
args...,
).Scan(&total); err != nil {
http.Error(w, fmt.Sprintf("db count error: %v", err), http.StatusInternalServerError)
return
}
dataArgs := append(args, limit, (page-1)*limit)
rows, err := db.Query(
fmt.Sprintf(
`SELECT id, user_id, action, resource, ip, timestamp, details
FROM audit_logs %s
ORDER BY timestamp DESC
LIMIT $%d OFFSET $%d`,
where, idx, idx+1,
),
dataArgs...,
)
if err != nil {
http.Error(w, fmt.Sprintf("db query error: %v", err), http.StatusInternalServerError)
return
}
defer rows.Close()
entries := []AuditEntry{}
for rows.Next() {
var e AuditEntry
if err := rows.Scan(&e.ID, &e.UserID, &e.Action, &e.Resource, &e.IP, &e.Timestamp, &e.Details); err != nil {
http.Error(w, fmt.Sprintf("db scan error: %v", err), http.StatusInternalServerError)
return
}
entries = append(entries, e)
}
if err := rows.Err(); err != nil {
http.Error(w, fmt.Sprintf("db rows error: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(AuditListResponse{
Data: entries,
Total: total,
Page: page,
Limit: limit,
})
}
}
// LogAudit inserts one audit record. Non-fatal: caller may log the returned error.
func LogAudit(db *sql.DB, userID, action, resource, ip, details string) error {
id, err := auditUUID()
if err != nil {
return fmt.Errorf("generate id: %w", err)
}
_, err = db.Exec(
`INSERT INTO audit_logs (id, user_id, action, resource, ip, timestamp, details)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
id, userID, action, resource, ip, time.Now().UTC(), details,
)
return err
}
func auditUUID() (string, error) {
var b [16]byte
if _, err := rand.Read(b[:]); err != nil {
return "", err
}
b[6] = (b[6] & 0x0f) | 0x40 // version 4
b[8] = (b[8] & 0x3f) | 0x80 // variant
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:]), nil
}
func parsePositiveInt(s string, defaultVal int) (int, error) {
if s == "" {
return defaultVal, nil
}
v, err := strconv.Atoi(s)
if err != nil || v < 1 {
return 0, fmt.Errorf("must be a positive integer")
}
return v, nil
}