init push

This commit is contained in:
2026-07-16 22:16:45 +07:00
commit 8b068bdb10
1021 changed files with 332816 additions and 0 deletions

View File

@@ -0,0 +1,359 @@
package handlers
import (
"encoding/json"
"fmt"
"slices"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"wucher/internal/domain/audit"
"wucher/internal/service"
sharedconst "wucher/internal/shared/const"
"wucher/internal/shared/pkg/uuidv7"
"wucher/internal/transport/http/dto"
"wucher/internal/transport/http/jsonapi"
"wucher/internal/transport/http/response"
)
type AuditHandler struct {
svc *service.AuditLogService
}
func NewAuditHandler(svc *service.AuditLogService) *AuditHandler {
return &AuditHandler{svc: svc}
}
// ListAuditLogs godoc
// @Summary List audit logs
// @Description JSON:API list with pagination, filtering and sorting.
// @Tags Audit Logs
// @Produce json
// @Param filter[search] query string false "Search by request_id/layer/action/method/path/message (contains)"
// @Param filter[method] query string false "Method filter; single/comma-separated values or ALL/*" Enums(POST,PUT,PATCH,DELETE,UPDATE,ALL,*)
// @Param method query string false "Alias for filter[method]" Enums(POST,PUT,PATCH,DELETE,UPDATE,ALL,*)
// @Param filter[module] query string false "Module filter; single/comma-separated values or ALL/*" Enums(air_rescuer_checklist,audit_log,auth,base,base_regular,base_hems,base_roster,branding,contact,duty_roster,facility,federal_state,file_manager,flight,flight_data,forces_present,health_insurance_company,helicopter,helicopter_file,hems_roster,hospital,icao,insurance_patient_data,land,master_setting,medicine,mission,opc,patient_data,reserve_ac,role,user,vocation,unknown,ALL,*)
// @Param module query string false "Alias for filter[module]" Enums(air_rescuer_checklist,audit_log,auth,base,base_regular,base_hems,base_roster,branding,contact,duty_roster,facility,federal_state,file_manager,flight,flight_data,forces_present,health_insurance_company,helicopter,helicopter_file,hems_roster,hospital,icao,insurance_patient_data,land,master_setting,medicine,mission,opc,patient_data,reserve_ac,role,user,vocation,unknown,ALL,*)
// @Param filter[action] query string false "Action filter; single/comma-separated values or ALL/*" Enums(list,get,create,update,delete,export,custom,ALL,*)
// @Param action query string false "Alias for filter[action]" Enums(list,get,create,update,delete,export,custom,ALL,*)
// @Param page[number] query int false "Page number (default 1)"
// @Param page[size] query int false "Page size (default 20, max 200)"
// @Param sort query string false "Sort order" Enums(created_at,-created_at,module,-module,action,-action,layer,-layer,status_code,-status_code,success,-success)
// @Success 200 {object} jsonapi.Document
// @Failure 400 {object} dto.ErrorResponse
// @Failure 422 {object} dto.ErrorResponse
// @Router /api/v1/audit-logs/get-all [get]
func (h *AuditHandler) List(c *fiber.Ctx) error {
filter := c.Query("filter[search]")
if filter == "" {
filter = c.Query("filter[action]")
}
if filter == "" {
filter = c.Query("filter[layer]")
}
methodFilter := c.Query("filter[method]")
if methodFilter == "" {
methodFilter = c.Query("method")
}
methods := parseAuditMethodFilter(methodFilter)
moduleFilter := c.Query("filter[module]")
if moduleFilter == "" {
moduleFilter = c.Query("module")
}
modules := parseAuditTokenFilter(moduleFilter)
actionFilter := c.Query("filter[action]")
if actionFilter == "" {
actionFilter = c.Query("action")
}
actions := parseAuditTokenFilter(actionFilter)
pageNumber := parseIntDefault(c.Query("page[number]"), 1)
pageSize := parseIntDefault(c.Query("page[size]"), 20)
if pageSize > 200 {
pageSize = 200
}
if pageNumber < 1 {
pageNumber = 1
}
offset := (pageNumber - 1) * pageSize
_ = offset
sort := c.Query("sort")
if errObj := validateAuditListQuery(methods, modules, actions, sort); errObj != nil {
return response.WriteErrors(c, fiber.StatusUnprocessableEntity, []jsonapi.ErrorObject{*errObj})
}
logs, total, err := h.svc.List(c.UserContext(), filter, methods, modules, actions, sort, 0, 0)
if err != nil {
return response.WriteErrors(c, fiber.StatusBadRequest, []jsonapi.ErrorObject{{
Status: "400",
Title: "List failed",
Detail: safeInternalDetail(),
}})
}
data := make([]dto.AuditLogResource, 0, len(logs))
for i := range logs {
data = append(data, auditLogResource(&logs[i]))
}
meta := map[string]any{
"total": total,
}
return response.WriteWithMeta(c, fiber.StatusOK, data, meta)
}
func parseAuditMethodFilter(raw string) []string {
v := strings.TrimSpace(raw)
if v == "" {
return nil
}
parts := strings.Split(v, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
token := strings.TrimSpace(part)
if token == "" {
continue
}
out = append(out, token)
}
return out
}
func auditLogResource(v *audit.AuditLog) dto.AuditLogResource {
id, _ := uuidv7.BytesToString(v.ID)
actorID := ""
if len(v.ActorUserID) == 16 {
actorID, _ = uuidv7.BytesToString(v.ActorUserID)
}
var metadata any
if len(v.Metadata) > 0 {
_ = json.Unmarshal(v.Metadata, &metadata)
}
module := strings.TrimSpace(v.Module)
if module == "" {
module = inferAuditModuleFromPath(strings.TrimSpace(v.Path))
}
return dto.AuditLogResource{
Type: "audit_log",
ID: id,
Attributes: dto.AuditLogAttributes{
RequestID: strings.TrimSpace(v.RequestID),
ActorUserID: actorID,
Layer: strings.TrimSpace(v.Layer),
Module: module,
Action: strings.TrimSpace(v.Action),
Method: strings.TrimSpace(v.Method),
Path: strings.TrimSpace(v.Path),
StatusCode: v.StatusCode,
Success: v.Success,
IP: strings.TrimSpace(v.IP),
UserAgent: strings.TrimSpace(v.UserAgent),
Message: strings.TrimSpace(v.Message),
Metadata: metadata,
CreatedAt: v.CreatedAt.UTC().Format(time.RFC3339),
},
}
}
func inferAuditModuleFromPath(path string) string {
parts := strings.Split(strings.Trim(strings.ToLower(strings.TrimSpace(path)), "/"), "/")
if len(parts) < 3 || parts[0] != "api" || parts[1] != "v1" {
return sharedconst.AuditModuleUnknown
}
switch parts[2] {
case "air-rescuer-checklist":
return sharedconst.AuditModuleAirRescuerChecklist
case "audit-logs":
return sharedconst.AuditModuleAuditLog
case "auth":
return sharedconst.AuditModuleAuth
case "bases":
return sharedconst.AuditModuleBase
case "branding", "public":
return sharedconst.AuditModuleBranding
case "contacts":
return sharedconst.AuditModuleContact
case "duty-roster":
return sharedconst.AuditModuleDutyRoster
case "dul":
return sharedconst.AuditModuleDUL
case "facilities":
return sharedconst.AuditModuleFacility
case "federal-state":
return sharedconst.AuditModuleFederalState
case "file-manager":
return sharedconst.AuditModuleFileManager
case "flight-data":
return sharedconst.AuditModuleFlightData
case "flights":
return sharedconst.AuditModuleFlight
case "forces-present":
return sharedconst.AuditModuleForcesPresent
case "health-insurance-companies":
return sharedconst.AuditModuleHealthInsuranceCompany
case "helicopter-files":
return sharedconst.AuditModuleHelicopterFile
case "helicopters":
return sharedconst.AuditModuleHelicopter
case "hems-operation", "hems-operation-category", "hems-operational-data":
return sharedconst.AuditModuleUnknown
case "hospital":
return sharedconst.AuditModuleHospital
case "icao":
return sharedconst.AuditModuleICAO
case "insurance-patient-data":
return sharedconst.AuditModuleInsurancePatientData
case "land":
return sharedconst.AuditModuleLand
case "master-settings":
return sharedconst.AuditModuleMasterSetting
case "medicine":
return sharedconst.AuditModuleMedicine
case "mission":
return sharedconst.AuditModuleMission
case "opc":
return sharedconst.AuditModuleOPC
case "patient-data":
return sharedconst.AuditModulePatientData
case "reserve-acs":
return sharedconst.AuditModuleReserveAC
case "roles":
return sharedconst.AuditModuleRole
case "users":
return sharedconst.AuditModuleUser
case "vocation":
return sharedconst.AuditModuleVocation
default:
return sharedconst.AuditModuleUnknown
}
}
func parseAuditTokenFilter(raw string) []string {
v := strings.TrimSpace(raw)
if v == "" {
return nil
}
parts := strings.Split(v, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
token := strings.TrimSpace(part)
if token == "" {
continue
}
out = append(out, token)
}
return out
}
func validateAuditListQuery(methods, modules, actions []string, sort string) *jsonapi.ErrorObject {
for _, method := range methods {
v := strings.ToUpper(strings.TrimSpace(method))
if v == "" || v == "ALL" || v == "*" {
continue
}
if !slices.Contains([]string{"POST", "PUT", "PATCH", "DELETE", "UPDATE"}, v) {
return &jsonapi.ErrorObject{
Status: "422",
Title: "Validation error",
Detail: fmt.Sprintf("invalid method filter: %s", method),
Source: &jsonapi.ErrorSource{Pointer: "/query/method"},
}
}
}
validModules := []string{
sharedconst.AuditModuleAirRescuerChecklist,
sharedconst.AuditModuleAuditLog,
sharedconst.AuditModuleAuth,
sharedconst.AuditModuleBase,
sharedconst.AuditModuleBaseRegular,
sharedconst.AuditModuleBaseHEMS,
sharedconst.AuditModuleBranding,
sharedconst.AuditModuleContact,
sharedconst.AuditModuleDutyRoster,
sharedconst.AuditModuleDUL,
sharedconst.AuditModuleFacility,
sharedconst.AuditModuleFederalState,
sharedconst.AuditModuleFileManager,
sharedconst.AuditModuleFlightData,
sharedconst.AuditModuleFlight,
sharedconst.AuditModuleForcesPresent,
sharedconst.AuditModuleHealthInsuranceCompany,
sharedconst.AuditModuleHelicopterFile,
sharedconst.AuditModuleHelicopter,
sharedconst.AuditModuleHospital,
sharedconst.AuditModuleICAO,
sharedconst.AuditModuleInsurancePatientData,
sharedconst.AuditModuleLand,
sharedconst.AuditModuleMasterSetting,
sharedconst.AuditModuleMedicine,
sharedconst.AuditModuleMission,
sharedconst.AuditModuleOPC,
sharedconst.AuditModulePatientData,
sharedconst.AuditModuleReserveAC,
sharedconst.AuditModuleRole,
sharedconst.AuditModuleUser,
sharedconst.AuditModuleVocation,
sharedconst.AuditModuleUnknown,
}
for _, module := range modules {
v := strings.ToLower(strings.TrimSpace(module))
if v == "" || v == "all" || v == "*" {
continue
}
if !slices.Contains(validModules, v) {
return &jsonapi.ErrorObject{
Status: "422",
Title: "Validation error",
Detail: fmt.Sprintf("invalid module filter: %s", module),
Source: &jsonapi.ErrorSource{Pointer: "/query/module"},
}
}
}
validActions := []string{
sharedconst.AuditActionList,
sharedconst.AuditActionGet,
sharedconst.AuditActionCreate,
sharedconst.AuditActionUpdate,
sharedconst.AuditActionDelete,
sharedconst.AuditActionExport,
sharedconst.AuditActionCustom,
}
for _, action := range actions {
v := strings.ToLower(strings.TrimSpace(action))
if v == "" || v == "all" || v == "*" {
continue
}
if !slices.Contains(validActions, v) {
return &jsonapi.ErrorObject{
Status: "422",
Title: "Validation error",
Detail: fmt.Sprintf("invalid action filter: %s", action),
Source: &jsonapi.ErrorSource{Pointer: "/query/action"},
}
}
}
if strings.TrimSpace(sort) != "" {
allowed := map[string]struct{}{
"created_at": {}, "-created_at": {},
"module": {}, "-module": {},
"action": {}, "-action": {},
"layer": {}, "-layer": {},
"status_code": {}, "-status_code": {},
"success": {}, "-success": {},
}
if _, ok := allowed[strings.TrimSpace(sort)]; !ok {
return &jsonapi.ErrorObject{
Status: "422",
Title: "Validation error",
Detail: fmt.Sprintf("invalid sort: %s", sort),
Source: &jsonapi.ErrorSource{Pointer: "/query/sort"},
}
}
}
return nil
}