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

@@ -38,8 +38,17 @@ const (
// ClaimsFromContext retrieves the validated JWT claims from the request context.
// Returns nil if no claims are present (unauthenticated request).
//
// Security: The type assertion uses the ok form so a context value of the wrong
// type (e.g. from a different package's context injection) returns nil rather
// than panicking.
func ClaimsFromContext(ctx context.Context) *token.Claims {
c, _ := ctx.Value(claimsKey).(*token.Claims)
// ok is intentionally checked: if the value is absent or the wrong type,
// c is nil (zero value for *token.Claims), which is the correct "no auth" result.
c, ok := ctx.Value(claimsKey).(*token.Claims)
if !ok {
return nil
}
return c
}
@@ -152,18 +161,18 @@ func RequireRole(role string) func(http.Handler) http.Handler {
// rateLimitEntry holds the token bucket state for a single IP.
type rateLimitEntry struct {
tokens float64
lastSeen time.Time
tokens float64
mu sync.Mutex
}
// ipRateLimiter implements a per-IP token bucket rate limiter.
type ipRateLimiter struct {
rps float64 // refill rate: tokens per second
burst float64 // bucket capacity
ttl time.Duration // how long to keep idle entries
mu sync.Mutex
ips map[string]*rateLimitEntry
rps float64
burst float64
ttl time.Duration
mu sync.Mutex
}
// RateLimit returns middleware implementing a per-IP token bucket.

View File

@@ -314,14 +314,14 @@ func TestExtractBearerToken(t *testing.T) {
tests := []struct {
name string
header string
wantErr bool
want string
wantErr bool
}{
{"valid", "Bearer mytoken123", false, "mytoken123"},
{"missing header", "", true, ""},
{"no bearer prefix", "Token mytoken123", true, ""},
{"empty token", "Bearer ", true, ""},
{"case insensitive", "bearer mytoken123", false, "mytoken123"},
{"valid", "Bearer mytoken123", "mytoken123", false},
{"missing header", "", "", true},
{"no bearer prefix", "Token mytoken123", "", true},
{"empty token", "Bearer ", "", true},
{"case insensitive", "bearer mytoken123", "mytoken123", false},
}
for _, tc := range tests {