feat(server/ops-api): add operations console backend
Stand up an internal ops-api service with operator account, auth, and audit log subsystems backed by a dedicated migrations_ops migration set. Wire Makefile targets and the migrate Dockerfile stage so the new schema ships alongside the existing tenant + monitoring databases. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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"`
|
||||
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,
|
||||
UserAgent: l.UserAgent,
|
||||
RequestID: l.RequestID,
|
||||
CreatedAt: l.CreatedAt,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user