Implement dashboard and audit log templates, add paginated audit log support
- Added `web/templates/{dashboard,audit,base,accounts,account_detail}.html` for a consistent UI.
- Implemented new audit log endpoint (`GET /v1/audit`) with filtering and pagination via `ListAuditEventsPaged`.
- Extended `AuditQueryParams`, added `AuditEventView` for joined actor/target usernames.
- Updated configuration (`goimports` preference), linting rules, and E2E tests.
- No logic changes to existing APIs.
This commit is contained in:
@@ -30,10 +30,10 @@ import (
|
||||
type Server struct {
|
||||
db *db.DB
|
||||
cfg *config.Config
|
||||
logger *slog.Logger
|
||||
privKey ed25519.PrivateKey
|
||||
pubKey ed25519.PublicKey
|
||||
masterKey []byte
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// New creates a Server with the given dependencies.
|
||||
@@ -83,6 +83,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.Handle("PUT /v1/accounts/{id}/roles", requireAdmin(http.HandlerFunc(s.handleSetRoles)))
|
||||
mux.Handle("GET /v1/accounts/{id}/pgcreds", requireAdmin(http.HandlerFunc(s.handleGetPGCreds)))
|
||||
mux.Handle("PUT /v1/accounts/{id}/pgcreds", requireAdmin(http.HandlerFunc(s.handleSetPGCreds)))
|
||||
mux.Handle("GET /v1/audit", requireAdmin(http.HandlerFunc(s.handleListAudit)))
|
||||
|
||||
// Apply global middleware: logging and login-path rate limiting.
|
||||
var root http.Handler = mux
|
||||
@@ -294,10 +295,10 @@ type validateRequest struct {
|
||||
}
|
||||
|
||||
type validateResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
Subject string `json:"sub,omitempty"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
Roles []string `json:"roles,omitempty"`
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
func (s *Server) handleTokenValidate(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -422,9 +423,9 @@ type accountResponse struct {
|
||||
Username string `json:"username"`
|
||||
AccountType string `json:"account_type"`
|
||||
Status string `json:"status"`
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
TOTPEnabled bool `json:"totp_enabled"`
|
||||
}
|
||||
|
||||
func accountToResponse(a *model.Account) accountResponse {
|
||||
@@ -727,10 +728,10 @@ func (s *Server) handleTOTPRemove(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
type pgCredRequest struct {
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Database string `json:"database"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func (s *Server) handleGetPGCreds(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -802,6 +803,72 @@ func (s *Server) handleSetPGCreds(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// ---- Audit endpoints ----
|
||||
|
||||
// handleListAudit returns paginated audit log entries with resolved usernames.
|
||||
// Query params: limit (1-200, default 50), offset, event_type, actor_id (UUID).
|
||||
func (s *Server) handleListAudit(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
|
||||
limit := parseIntParam(q.Get("limit"), 50)
|
||||
if limit < 1 {
|
||||
limit = 1
|
||||
}
|
||||
if limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
offset := parseIntParam(q.Get("offset"), 0)
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
params := db.AuditQueryParams{
|
||||
EventType: q.Get("event_type"),
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
|
||||
// Resolve actor_id from UUID to internal int64.
|
||||
if actorUUID := q.Get("actor_id"); actorUUID != "" {
|
||||
acct, err := s.db.GetAccountByUUID(actorUUID)
|
||||
if err == nil {
|
||||
params.AccountID = &acct.ID
|
||||
}
|
||||
// If actor_id is provided but not found, return empty results (correct behaviour).
|
||||
}
|
||||
|
||||
events, total, err := s.db.ListAuditEventsPaged(params)
|
||||
if err != nil {
|
||||
s.logger.Error("list audit events", "error", err)
|
||||
middleware.WriteError(w, http.StatusInternalServerError, "internal error", "internal_error")
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure a nil slice serialises as [] rather than null.
|
||||
if events == nil {
|
||||
events = []*db.AuditEventView{}
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"events": events,
|
||||
"total": total,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
})
|
||||
}
|
||||
|
||||
// parseIntParam parses a query parameter as an int, returning defaultVal on failure.
|
||||
func parseIntParam(s string, defaultVal int) int {
|
||||
if s == "" {
|
||||
return defaultVal
|
||||
}
|
||||
var v int
|
||||
if _, err := fmt.Sscanf(s, "%d", &v); err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// ---- Helpers ----
|
||||
|
||||
// loadAccount retrieves an account by the {id} path parameter (UUID).
|
||||
|
||||
Reference in New Issue
Block a user