Files
oslstats/internal/handlers/admin_audit.go
2026-02-14 19:48:59 +11:00

259 lines
6.7 KiB
Go

package handlers
import (
"context"
"net/http"
"strconv"
"time"
"git.haelnorr.com/h/golib/hws"
"git.haelnorr.com/h/oslstats/internal/db"
"git.haelnorr.com/h/oslstats/internal/throw"
"git.haelnorr.com/h/oslstats/internal/validation"
adminview "git.haelnorr.com/h/oslstats/internal/view/adminview"
"git.haelnorr.com/h/timefmt"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
// AdminAuditLogsPage renders the full admin dashboard page with audit logs section (GET request)
func AdminAuditLogsPage(s *hws.Server, conn *db.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pageOpts, ok := db.GetPageOpts(s, w, r)
if !ok {
return
}
var logs *db.List[db.AuditLog]
var users []*db.User
var actions []string
var resourceTypes []string
if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
var err error
// Get filters from query
filters, ok := getAuditFiltersFromQuery(s, w, r)
if !ok {
return false, nil
}
// Get audit logs
logs, err = db.GetAuditLogs(ctx, tx, pageOpts, filters)
if err != nil {
return false, errors.Wrap(err, "db.GetAuditLogs")
}
// Get all users for filter dropdown
usersList, err := db.GetUsers(ctx, tx, nil)
if err != nil {
return false, errors.Wrap(err, "db.GetUsers")
}
users = usersList.Items
// Get unique actions
actions, err = db.GetUniqueActions(ctx, tx)
if err != nil {
return false, errors.Wrap(err, "db.GetUniqueActions")
}
// Get unique resource types
resourceTypes, err = db.GetUniqueResourceTypes(ctx, tx)
if err != nil {
return false, errors.Wrap(err, "db.GetUniqueResourceTypes")
}
return true, nil
}); !ok {
return
}
renderSafely(adminview.AuditLogsPage(logs, users, actions, resourceTypes), s, r, w)
})
}
// AdminAuditLogsList shows the full audit logs list with filters (POST request for HTMX)
func AdminAuditLogsList(s *hws.Server, conn *db.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pageOpts, ok := db.GetPageOpts(s, w, r)
if !ok {
return
}
var logs *db.List[db.AuditLog]
var users []*db.User
var actions []string
var resourceTypes []string
if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
var err error
// Get filters from form
filters, ok := getAuditFiltersFromForm(s, w, r)
if !ok {
return false, nil
}
// Get audit logs
logs, err = db.GetAuditLogs(ctx, tx, pageOpts, filters)
if err != nil {
return false, errors.Wrap(err, "db.GetAuditLogs")
}
// Get all users for filter dropdown
usersList, err := db.GetUsers(ctx, tx, nil)
if err != nil {
return false, errors.Wrap(err, "db.GetUsers")
}
users = usersList.Items
// Get unique actions
actions, err = db.GetUniqueActions(ctx, tx)
if err != nil {
return false, errors.Wrap(err, "db.GetUniqueActions")
}
// Get unique resource types
resourceTypes, err = db.GetUniqueResourceTypes(ctx, tx)
if err != nil {
return false, errors.Wrap(err, "db.GetUniqueResourceTypes")
}
return true, nil
}); !ok {
return
}
renderSafely(adminview.AuditLogsList(logs, users, actions, resourceTypes), s, r, w)
})
}
// AdminAuditLogsFilter returns only the results container (table + pagination) for HTMX updates
func AdminAuditLogsFilter(s *hws.Server, conn *db.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pageOpts, ok := db.GetPageOpts(s, w, r)
if !ok {
return
}
var logs *db.List[db.AuditLog]
if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
var err error
// Get filters from form
filters, ok := getAuditFiltersFromForm(s, w, r)
if !ok {
return false, nil
}
// Get audit logs
logs, err = db.GetAuditLogs(ctx, tx, pageOpts, filters)
if err != nil {
return false, errors.Wrap(err, "db.GetAuditLogs")
}
return true, nil
}); !ok {
return
}
// Return only the results container, not the full page with filters
renderSafely(adminview.AuditLogsResults(logs), s, r, w)
})
}
// AdminAuditLogDetail shows details for a single audit log entry
func AdminAuditLogDetail(s *hws.Server, conn *db.DB) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get ID from path
idStr := r.PathValue("id")
if idStr == "" {
throw.BadRequest(s, w, r, "Missing audit log ID", nil)
return
}
id, err := strconv.Atoi(idStr)
if err != nil {
throw.BadRequest(s, w, r, "Invalid audit log ID", err)
return
}
var log *db.AuditLog
if ok := conn.WithReadTx(s, w, r, func(ctx context.Context, tx bun.Tx) (bool, error) {
var err error
log, err = db.GetAuditLogByID(ctx, tx, id)
if err != nil {
return false, errors.Wrap(err, "db.GetAuditLogByID")
}
if log == nil {
throw.NotFound(s, w, r, r.URL.Path)
return false, nil
}
return true, nil
}); !ok {
return
}
renderSafely(adminview.AuditLogDetail(log), s, r, w)
})
}
// getAuditFiltersFromQuery extracts audit log filters from query string
func getAuditFiltersFromQuery(s *hws.Server, w http.ResponseWriter, r *http.Request) (*db.AuditLogFilter, bool) {
g := validation.NewQueryGetter(r)
filters, ok := buildAuditFilters(g, s, w, r)
return filters, ok
}
// getAuditFiltersFromForm extracts audit log filters from form data
func getAuditFiltersFromForm(s *hws.Server, w http.ResponseWriter, r *http.Request) (*db.AuditLogFilter, bool) {
g, ok := validation.ParseFormOrError(s, w, r)
if !ok {
return nil, false
}
return buildAuditFilters(g, s, w, r)
}
// buildAuditFilters builds audit log filters from a validation.Getter
func buildAuditFilters(g validation.Getter, s *hws.Server, w http.ResponseWriter, r *http.Request) (*db.AuditLogFilter, bool) {
filters := db.NewAuditLogFilter()
userIDs := g.IntList("user_id").Values()
actions := g.StringList("action").Values()
resourceTypes := g.StringList("resource_type").Values()
results := g.StringList("result").Values()
format := timefmt.NewBuilder().DayNumeric2().Slash().
MonthNumeric2().Slash().Year4().Build()
startDate := g.Time("start_date", format).Optional().Value
endDate := g.Time("end_date", format).Optional().Value
if !g.ValidateAndError(s, w, r) {
return nil, false
}
if len(userIDs) > 0 {
filters.UserIDs(userIDs)
}
if len(actions) > 0 {
filters.Actions(actions)
}
if len(resourceTypes) > 0 {
filters.ResourceTypes(resourceTypes)
}
if len(results) > 0 {
filters.Results(results)
}
if !startDate.IsZero() {
filters.DateRange(startDate.Unix(), 0)
}
if !endDate.IsZero() {
endOfDay := endDate.Add(23*time.Hour + 59*time.Minute + 59*time.Second)
filters.DateRange(0, endOfDay.Unix())
}
return filters, true
}