feat: add wallet settlement audit flow
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type AuditLog struct {
|
||||
ID string `json:"id"`
|
||||
Category string `json:"category"`
|
||||
Action string `json:"action"`
|
||||
ActorGatewayUserID string `json:"actorGatewayUserId,omitempty"`
|
||||
ActorUserID string `json:"actorUserId,omitempty"`
|
||||
ActorUsername string `json:"actorUsername,omitempty"`
|
||||
ActorSource string `json:"actorSource,omitempty"`
|
||||
ActorRoles []string `json:"actorRoles,omitempty"`
|
||||
TargetType string `json:"targetType"`
|
||||
TargetID string `json:"targetId"`
|
||||
TargetGatewayUserID string `json:"targetGatewayUserId,omitempty"`
|
||||
TargetGatewayTenantID string `json:"targetGatewayTenantId,omitempty"`
|
||||
RequestIP string `json:"requestIp,omitempty"`
|
||||
UserAgent string `json:"userAgent,omitempty"`
|
||||
BeforeState map[string]any `json:"beforeState,omitempty"`
|
||||
AfterState map[string]any `json:"afterState,omitempty"`
|
||||
Metadata map[string]any `json:"metadata,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type AuditLogInput struct {
|
||||
Category string `json:"category"`
|
||||
Action string `json:"action"`
|
||||
ActorGatewayUserID string `json:"actorGatewayUserId"`
|
||||
ActorUserID string `json:"actorUserId"`
|
||||
ActorUsername string `json:"actorUsername"`
|
||||
ActorSource string `json:"actorSource"`
|
||||
ActorRoles []string `json:"actorRoles"`
|
||||
TargetType string `json:"targetType"`
|
||||
TargetID string `json:"targetId"`
|
||||
TargetGatewayUserID string `json:"targetGatewayUserId"`
|
||||
TargetGatewayTenantID string `json:"targetGatewayTenantId"`
|
||||
RequestIP string `json:"requestIp"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
BeforeState map[string]any `json:"beforeState"`
|
||||
AfterState map[string]any `json:"afterState"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
}
|
||||
|
||||
type AuditLogFilter struct {
|
||||
Category string
|
||||
Action string
|
||||
TargetType string
|
||||
TargetID string
|
||||
Limit int
|
||||
}
|
||||
|
||||
func (s *Store) RecordAuditLog(ctx context.Context, input AuditLogInput) (AuditLog, error) {
|
||||
return s.RecordAuditLogTx(ctx, s.pool, input)
|
||||
}
|
||||
|
||||
func (s *Store) RecordAuditLogTx(ctx context.Context, tx Tx, input AuditLogInput) (AuditLog, error) {
|
||||
input = normalizeAuditLogInput(input)
|
||||
actorRolesJSON, _ := json.Marshal(input.ActorRoles)
|
||||
beforeJSON, _ := json.Marshal(emptyObjectIfNil(input.BeforeState))
|
||||
afterJSON, _ := json.Marshal(emptyObjectIfNil(input.AfterState))
|
||||
metadataJSON, _ := json.Marshal(emptyObjectIfNil(input.Metadata))
|
||||
return scanAuditLog(tx.QueryRow(ctx, `
|
||||
INSERT INTO gateway_audit_logs (
|
||||
category, action, actor_gateway_user_id, actor_user_id, actor_username, actor_source,
|
||||
actor_roles, target_type, target_id, target_gateway_user_id, target_gateway_tenant_id,
|
||||
request_ip, user_agent, before_state, after_state, metadata
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, NULLIF($3, '')::uuid, NULLIF($4, ''), NULLIF($5, ''), NULLIF($6, ''),
|
||||
$7::jsonb, $8, $9, NULLIF($10, '')::uuid, NULLIF($11, '')::uuid,
|
||||
NULLIF($12, ''), NULLIF($13, ''), $14::jsonb, $15::jsonb, $16::jsonb
|
||||
)
|
||||
RETURNING id::text, category, action, COALESCE(actor_gateway_user_id::text, ''), COALESCE(actor_user_id, ''),
|
||||
COALESCE(actor_username, ''), COALESCE(actor_source, ''), actor_roles,
|
||||
target_type, target_id, COALESCE(target_gateway_user_id::text, ''),
|
||||
COALESCE(target_gateway_tenant_id::text, ''), COALESCE(request_ip, ''),
|
||||
COALESCE(user_agent, ''), before_state, after_state, metadata, created_at`,
|
||||
input.Category,
|
||||
input.Action,
|
||||
input.ActorGatewayUserID,
|
||||
input.ActorUserID,
|
||||
input.ActorUsername,
|
||||
input.ActorSource,
|
||||
string(actorRolesJSON),
|
||||
input.TargetType,
|
||||
input.TargetID,
|
||||
input.TargetGatewayUserID,
|
||||
input.TargetGatewayTenantID,
|
||||
input.RequestIP,
|
||||
input.UserAgent,
|
||||
string(beforeJSON),
|
||||
string(afterJSON),
|
||||
string(metadataJSON),
|
||||
))
|
||||
}
|
||||
|
||||
func (s *Store) ListAuditLogs(ctx context.Context, filter AuditLogFilter) ([]AuditLog, error) {
|
||||
limit := filter.Limit
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
if limit > 500 {
|
||||
limit = 500
|
||||
}
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id::text, category, action, COALESCE(actor_gateway_user_id::text, ''), COALESCE(actor_user_id, ''),
|
||||
COALESCE(actor_username, ''), COALESCE(actor_source, ''), actor_roles,
|
||||
target_type, target_id, COALESCE(target_gateway_user_id::text, ''),
|
||||
COALESCE(target_gateway_tenant_id::text, ''), COALESCE(request_ip, ''),
|
||||
COALESCE(user_agent, ''), before_state, after_state, metadata, created_at
|
||||
FROM gateway_audit_logs
|
||||
WHERE (NULLIF($1, '') IS NULL OR category = $1)
|
||||
AND (NULLIF($2, '') IS NULL OR action = $2)
|
||||
AND (NULLIF($3, '') IS NULL OR target_type = $3)
|
||||
AND (NULLIF($4, '') IS NULL OR target_id = $4)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $5`,
|
||||
strings.TrimSpace(filter.Category),
|
||||
strings.TrimSpace(filter.Action),
|
||||
strings.TrimSpace(filter.TargetType),
|
||||
strings.TrimSpace(filter.TargetID),
|
||||
limit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := make([]AuditLog, 0)
|
||||
for rows.Next() {
|
||||
item, err := scanAuditLog(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func normalizeAuditLogInput(input AuditLogInput) AuditLogInput {
|
||||
input.Category = firstNonEmpty(strings.TrimSpace(input.Category), "system")
|
||||
input.Action = strings.TrimSpace(input.Action)
|
||||
input.TargetType = strings.TrimSpace(input.TargetType)
|
||||
input.TargetID = strings.TrimSpace(input.TargetID)
|
||||
input.ActorSource = firstNonEmpty(strings.TrimSpace(input.ActorSource), "gateway")
|
||||
return input
|
||||
}
|
||||
|
||||
func scanAuditLog(row scanner) (AuditLog, error) {
|
||||
var item AuditLog
|
||||
var actorRoles []byte
|
||||
var beforeState []byte
|
||||
var afterState []byte
|
||||
var metadata []byte
|
||||
if err := row.Scan(
|
||||
&item.ID,
|
||||
&item.Category,
|
||||
&item.Action,
|
||||
&item.ActorGatewayUserID,
|
||||
&item.ActorUserID,
|
||||
&item.ActorUsername,
|
||||
&item.ActorSource,
|
||||
&actorRoles,
|
||||
&item.TargetType,
|
||||
&item.TargetID,
|
||||
&item.TargetGatewayUserID,
|
||||
&item.TargetGatewayTenantID,
|
||||
&item.RequestIP,
|
||||
&item.UserAgent,
|
||||
&beforeState,
|
||||
&afterState,
|
||||
&metadata,
|
||||
&item.CreatedAt,
|
||||
); err != nil {
|
||||
return AuditLog{}, err
|
||||
}
|
||||
item.ActorRoles = decodeStringArray(actorRoles)
|
||||
item.BeforeState = decodeObject(beforeState)
|
||||
item.AfterState = decodeObject(afterState)
|
||||
item.Metadata = decodeObject(metadata)
|
||||
return item, nil
|
||||
}
|
||||
Reference in New Issue
Block a user