Files
geo/server/internal/ops/transport/audit_handler.go
T
root 3480d04ec3 feat(ops-api/audit): annotate audit log IP with region lookup
Add an IPRegionResolver wrapping the ip2region xdb library and attach it
to AuditService so each appended event records both the raw IP and a
resolved region in a new ops.audit_logs.ip_region column. Loopback and
private addresses short-circuit to local labels; missing xdb data or
lookup errors degrade silently so auditing keeps working without it.
The ops console audit view shows the region beneath the IP. Bundle the
v4/v6 xdb data under internal/ops/app/ipregiondata so the resolver works
out of the box, with config paths/env overrides for swapping in updated
data sets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 00:46:37 +08:00

107 lines
2.7 KiB
Go

package transport
import (
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/geo-platform/tenant-api/internal/ops/app"
"github.com/geo-platform/tenant-api/internal/ops/domain"
"github.com/geo-platform/tenant-api/internal/shared/response"
)
func listAuditsHandler(svc *app.AuditService) gin.HandlerFunc {
return func(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
size, _ := strconv.Atoi(c.DefaultQuery("size", "50"))
if page <= 0 {
page = 1
}
if size <= 0 || size > 500 {
size = 50
}
filter := domain.AuditLogFilter{
Action: c.Query("action"),
Limit: size,
Offset: (page - 1) * size,
}
if v := c.Query("operator_id"); v != "" {
id, err := strconv.ParseInt(v, 10, 64)
if err != nil {
response.Error(c, response.ErrBadRequest(40002, "invalid_operator_id", "operator_id 不合法"))
return
}
filter.OperatorID = &id
}
if v := c.Query("start_at"); v != "" {
t, err := time.Parse(time.RFC3339, v)
if err != nil {
response.Error(c, response.ErrBadRequest(40003, "invalid_start_at", "start_at 必须是 RFC3339"))
return
}
filter.StartAt = &t
}
if v := c.Query("end_at"); v != "" {
t, err := time.Parse(time.RFC3339, v)
if err != nil {
response.Error(c, response.ErrBadRequest(40004, "invalid_end_at", "end_at 必须是 RFC3339"))
return
}
filter.EndAt = &t
}
items, total, err := svc.List(c.Request.Context(), filter)
if err != nil {
response.Error(c, err)
return
}
dtos := make([]auditLogDTO, 0, len(items))
for _, log := range items {
dtos = append(dtos, toAuditLogDTO(log))
}
response.Success(c, gin.H{
"items": dtos,
"total": total,
"page": page,
"size": size,
})
}
}
type auditLogDTO struct {
ID int64 `json:"id"`
OperatorID *int64 `json:"operator_id"`
OperatorName *string `json:"operator_name"`
Action string `json:"action"`
TargetType *string `json:"target_type"`
TargetID *string `json:"target_id"`
Metadata map[string]any `json:"metadata"`
IP *string `json:"ip"`
IPRegion *string `json:"ip_region"`
UserAgent *string `json:"user_agent"`
RequestID *string `json:"request_id"`
CreatedAt time.Time `json:"created_at"`
}
func toAuditLogDTO(l domain.AuditLog) auditLogDTO {
return auditLogDTO{
ID: l.ID,
OperatorID: l.OperatorID,
OperatorName: l.OperatorName,
Action: l.Action,
TargetType: l.TargetType,
TargetID: l.TargetID,
Metadata: l.Metadata,
IP: l.IP,
IPRegion: l.IPRegion,
UserAgent: l.UserAgent,
RequestID: l.RequestID,
CreatedAt: l.CreatedAt,
}
}