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:
2026-03-11 14:05:08 -07:00
parent 14083b82b4
commit e63d9863b6
20 changed files with 829 additions and 84 deletions

View File

@@ -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).