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,315 @@
package service
import (
"context"
"encoding/json"
"errors"
"log"
"strings"
"time"
"wucher/internal/domain/audit"
)
type AuditLogInput struct {
RequestID string
ActorUserID []byte
Layer string
Module string
Action string
Method string
Path string
StatusCode int
Success bool
IP string
UserAgent string
Message string
Metadata map[string]any
}
type AuditLogService struct {
repo audit.Repository
queue chan *audit.AuditLog
ipProtector SecretProtector
}
type AuditLogServiceDependencies struct {
QueueSize int
Workers int
IPEncryptionKey string
}
type auditLogListRepository interface {
List(ctx context.Context, filter string, methods []string, modules []string, actions []string, sort string, limit, offset int) ([]audit.AuditLog, int64, error)
}
func NewAuditLogService(repo audit.Repository, deps AuditLogServiceDependencies) *AuditLogService {
queueSize := deps.QueueSize
workers := deps.Workers
if queueSize <= 0 {
queueSize = 1024
}
if workers <= 0 {
workers = 2
}
key := strings.TrimSpace(deps.IPEncryptionKey)
svc := &AuditLogService{
repo: repo,
queue: make(chan *audit.AuditLog, queueSize),
ipProtector: newIPSecretProtector(key),
}
for i := 0; i < workers; i++ {
go svc.runWorker()
}
return svc
}
func (s *AuditLogService) Log(ctx context.Context, in AuditLogInput) {
if s == nil || s.repo == nil {
return
}
entry := &audit.AuditLog{
RequestID: truncate(strings.TrimSpace(in.RequestID), 64),
ActorUserID: safeUserID(in.ActorUserID),
Layer: truncate(strings.TrimSpace(in.Layer), 32),
Module: truncate(strings.TrimSpace(in.Module), 64),
Action: truncate(strings.TrimSpace(in.Action), 128),
Method: truncate(strings.TrimSpace(in.Method), 12),
Path: truncate(strings.TrimSpace(in.Path), 255),
StatusCode: in.StatusCode,
Success: in.Success,
IP: sanitizeIP(in.IP, s.ipProtector),
UserAgent: truncate(strings.TrimSpace(in.UserAgent), 255),
Message: truncate(strings.TrimSpace(in.Message), 512),
CreatedAt: time.Now().UTC(),
}
if len(in.Metadata) > 0 {
if b, err := json.Marshal(in.Metadata); err == nil {
entry.Metadata = truncateBytes(b, 8192)
}
}
select {
case s.queue <- entry:
default:
log.Printf("audit queue_full layer=%s action=%s", entry.Layer, entry.Action)
}
}
func (s *AuditLogService) List(ctx context.Context, filter string, methods []string, modules []string, actions []string, sort string, limit, offset int) ([]audit.AuditLog, int64, error) {
if s == nil || s.repo == nil {
return nil, 0, errors.New("audit service not configured")
}
lister, ok := s.repo.(auditLogListRepository)
if !ok {
return nil, 0, errors.New("audit listing not supported by repository")
}
if limit < 0 {
limit = 0
}
if limit > 200 {
limit = 200
}
if offset < 0 {
offset = 0
}
methods, actions = enforceAuditMutationFilters(methods, actions)
rows, total, err := lister.List(
ctx,
strings.TrimSpace(filter),
normalizeAuditMethods(methods),
normalizeAuditTokens(modules),
normalizeAuditTokens(actions),
normalizeAuditSort(sort),
limit,
offset,
)
if err != nil {
return nil, 0, err
}
for i := range rows {
rows[i].IP = decryptStoredIP(rows[i].IP, s.ipProtector)
}
return rows, total, nil
}
func enforceAuditMutationFilters(methods, actions []string) ([]string, []string) {
allowedMethods := []string{"POST", "PUT", "PATCH", "DELETE"}
allowedActions := []string{"create", "update", "delete"}
methods = enforceAllowedMethods(methods, allowedMethods)
actions = enforceAllowedTokens(actions, allowedActions)
return methods, actions
}
func enforceAllowedMethods(values []string, allowed []string) []string {
if len(values) == 0 {
return append([]string(nil), allowed...)
}
allowedSet := make(map[string]struct{}, len(allowed))
for _, value := range allowed {
allowedSet[strings.ToUpper(strings.TrimSpace(value))] = struct{}{}
}
out := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
v := strings.ToUpper(strings.TrimSpace(value))
if v == "UPDATE" {
if _, ok := allowedSet["PUT"]; ok {
if _, exists := seen["PUT"]; !exists {
seen["PUT"] = struct{}{}
out = append(out, "PUT")
}
}
if _, ok := allowedSet["PATCH"]; ok {
if _, exists := seen["PATCH"]; !exists {
seen["PATCH"] = struct{}{}
out = append(out, "PATCH")
}
}
continue
}
if _, ok := allowedSet[v]; !ok {
continue
}
if _, exists := seen[v]; exists {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
if len(out) == 0 {
return append([]string(nil), allowed...)
}
return out
}
func enforceAllowedTokens(values []string, allowed []string) []string {
if len(values) == 0 {
return append([]string(nil), allowed...)
}
allowedSet := make(map[string]struct{}, len(allowed))
for _, value := range allowed {
allowedSet[strings.ToLower(strings.TrimSpace(value))] = struct{}{}
}
out := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
v := strings.ToLower(strings.TrimSpace(value))
if _, ok := allowedSet[v]; !ok {
continue
}
if _, exists := seen[v]; exists {
continue
}
seen[v] = struct{}{}
out = append(out, v)
}
if len(out) == 0 {
return append([]string(nil), allowed...)
}
return out
}
func (s *AuditLogService) runWorker() {
for entry := range s.queue {
if entry == nil {
continue
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
err := s.repo.Create(ctx, entry)
cancel()
if err != nil {
log.Printf("audit persist_failed layer=%s action=%s err=%v", entry.Layer, entry.Action, err)
}
}
}
func safeUserID(id []byte) []byte {
if len(id) != 16 {
return nil
}
dst := make([]byte, 16)
copy(dst, id)
return dst
}
func truncateBytes(v []byte, max int) []byte {
if max <= 0 || len(v) <= max {
return v
}
out := make([]byte, max)
copy(out, v[:max])
return out
}
func normalizeAuditSort(sort string) string {
if normalized := normalizeSortByRules(strings.TrimSpace(sort),
sortField("created_at"),
sortField("action"),
sortField("module"),
sortField("layer"),
sortField("status_code"),
sortField("success"),
); normalized != "" {
return normalized
}
return "created_at DESC"
}
func normalizeAuditTokens(values []string) []string {
if len(values) == 0 {
return nil
}
seen := make(map[string]struct{}, len(values))
out := make([]string, 0, len(values))
for _, value := range values {
v := strings.ToLower(strings.TrimSpace(value))
switch v {
case "", "all", "*":
return nil
default:
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
out = append(out, v)
}
}
}
if len(out) == 0 {
return nil
}
return out
}
func normalizeAuditMethods(methods []string) []string {
if len(methods) == 0 {
return nil
}
seen := make(map[string]struct{}, len(methods))
out := make([]string, 0, len(methods))
for _, method := range methods {
v := strings.ToUpper(strings.TrimSpace(method))
switch v {
case "", "ALL", "*":
return nil
case "GET", "POST", "PUT", "PATCH", "DELETE":
if _, ok := seen[v]; !ok {
seen[v] = struct{}{}
out = append(out, v)
}
case "UPDATE":
if _, ok := seen["PUT"]; !ok {
seen["PUT"] = struct{}{}
out = append(out, "PUT")
}
if _, ok := seen["PATCH"]; !ok {
seen["PATCH"] = struct{}{}
out = append(out, "PATCH")
}
}
}
if len(out) == 0 {
return nil
}
return out
}