Files
breakpilot-core/consent-service/internal/handlers/admin_operations.go
Benjamin Admin 92c86ec6ba [split-required] [guardrail-change] Enforce 500 LOC budget across all services
Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 00:09:30 +02:00

320 lines
9.0 KiB
Go

package handlers
import (
"context"
"fmt"
"net/http"
"time"
"github.com/breakpilot/consent-service/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// ========================================
// ADMIN ENDPOINTS - Cookie Categories
// ========================================
// AdminGetCookieCategories returns all cookie categories
func (h *Handler) AdminGetCookieCategories(c *gin.Context) {
ctx := context.Background()
rows, err := h.db.Pool.Query(ctx, `
SELECT id, name, display_name_de, display_name_en, description_de, description_en,
is_mandatory, sort_order, is_active, created_at, updated_at
FROM cookie_categories
ORDER BY sort_order ASC
`)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch categories"})
return
}
defer rows.Close()
var categories []models.CookieCategory
for rows.Next() {
var cat models.CookieCategory
if err := rows.Scan(&cat.ID, &cat.Name, &cat.DisplayNameDE, &cat.DisplayNameEN,
&cat.DescriptionDE, &cat.DescriptionEN, &cat.IsMandatory, &cat.SortOrder,
&cat.IsActive, &cat.CreatedAt, &cat.UpdatedAt); err != nil {
continue
}
categories = append(categories, cat)
}
c.JSON(http.StatusOK, gin.H{"categories": categories})
}
// AdminCreateCookieCategory creates a new cookie category
func (h *Handler) AdminCreateCookieCategory(c *gin.Context) {
var req models.CreateCookieCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
ctx := context.Background()
var catID uuid.UUID
err := h.db.Pool.QueryRow(ctx, `
INSERT INTO cookie_categories (name, display_name_de, display_name_en, description_de, description_en, is_mandatory, sort_order)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id
`, req.Name, req.DisplayNameDE, req.DisplayNameEN, req.DescriptionDE, req.DescriptionEN, req.IsMandatory, req.SortOrder).Scan(&catID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create category"})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "Cookie category created successfully",
"id": catID,
})
}
// AdminUpdateCookieCategory updates a cookie category
func (h *Handler) AdminUpdateCookieCategory(c *gin.Context) {
catID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
var req struct {
DisplayNameDE *string `json:"display_name_de"`
DisplayNameEN *string `json:"display_name_en"`
DescriptionDE *string `json:"description_de"`
DescriptionEN *string `json:"description_en"`
IsMandatory *bool `json:"is_mandatory"`
SortOrder *int `json:"sort_order"`
IsActive *bool `json:"is_active"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
ctx := context.Background()
result, err := h.db.Pool.Exec(ctx, `
UPDATE cookie_categories
SET display_name_de = COALESCE($2, display_name_de),
display_name_en = COALESCE($3, display_name_en),
description_de = COALESCE($4, description_de),
description_en = COALESCE($5, description_en),
is_mandatory = COALESCE($6, is_mandatory),
sort_order = COALESCE($7, sort_order),
is_active = COALESCE($8, is_active),
updated_at = NOW()
WHERE id = $1
`, catID, req.DisplayNameDE, req.DisplayNameEN, req.DescriptionDE, req.DescriptionEN,
req.IsMandatory, req.SortOrder, req.IsActive)
if err != nil || result.RowsAffected() == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Cookie category updated successfully"})
}
// AdminDeleteCookieCategory soft-deletes a cookie category
func (h *Handler) AdminDeleteCookieCategory(c *gin.Context) {
catID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid category ID"})
return
}
ctx := context.Background()
result, err := h.db.Pool.Exec(ctx, `
UPDATE cookie_categories
SET is_active = false, updated_at = NOW()
WHERE id = $1
`, catID)
if err != nil || result.RowsAffected() == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Category not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Cookie category deleted successfully"})
}
// ========================================
// ADMIN ENDPOINTS - Statistics & Audit
// ========================================
// GetConsentStats returns consent statistics
func (h *Handler) GetConsentStats(c *gin.Context) {
ctx := context.Background()
docType := c.Query("document_type")
var stats models.ConsentStats
// Total users
h.db.Pool.QueryRow(ctx, `SELECT COUNT(*) FROM users`).Scan(&stats.TotalUsers)
// Consented users (with active consent)
query := `
SELECT COUNT(DISTINCT uc.user_id)
FROM user_consents uc
JOIN document_versions dv ON uc.document_version_id = dv.id
JOIN legal_documents ld ON dv.document_id = ld.id
WHERE uc.consented = true AND uc.withdrawn_at IS NULL
`
if docType != "" {
query += ` AND ld.type = $1`
h.db.Pool.QueryRow(ctx, query, docType).Scan(&stats.ConsentedUsers)
} else {
h.db.Pool.QueryRow(ctx, query).Scan(&stats.ConsentedUsers)
}
// Calculate consent rate
if stats.TotalUsers > 0 {
stats.ConsentRate = float64(stats.ConsentedUsers) / float64(stats.TotalUsers) * 100
}
// Recent consents (last 7 days)
h.db.Pool.QueryRow(ctx, `
SELECT COUNT(*) FROM user_consents
WHERE consented = true AND consented_at > NOW() - INTERVAL '7 days'
`).Scan(&stats.RecentConsents)
// Recent withdrawals
h.db.Pool.QueryRow(ctx, `
SELECT COUNT(*) FROM user_consents
WHERE withdrawn_at IS NOT NULL AND withdrawn_at > NOW() - INTERVAL '7 days'
`).Scan(&stats.RecentWithdrawals)
c.JSON(http.StatusOK, stats)
}
// GetCookieStats returns cookie consent statistics
func (h *Handler) GetCookieStats(c *gin.Context) {
ctx := context.Background()
rows, err := h.db.Pool.Query(ctx, `
SELECT cat.name,
COUNT(DISTINCT u.id) as total_users,
COUNT(DISTINCT CASE WHEN cc.consented = true THEN cc.user_id END) as consented_users
FROM cookie_categories cat
CROSS JOIN users u
LEFT JOIN cookie_consents cc ON cat.id = cc.category_id AND u.id = cc.user_id
WHERE cat.is_active = true
GROUP BY cat.id, cat.name
ORDER BY cat.sort_order
`)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch stats"})
return
}
defer rows.Close()
var stats []models.CookieStats
for rows.Next() {
var s models.CookieStats
if err := rows.Scan(&s.Category, &s.TotalUsers, &s.ConsentedUsers); err != nil {
continue
}
if s.TotalUsers > 0 {
s.ConsentRate = float64(s.ConsentedUsers) / float64(s.TotalUsers) * 100
}
stats = append(stats, s)
}
c.JSON(http.StatusOK, gin.H{"cookie_stats": stats})
}
// GetAuditLog returns audit log entries
func (h *Handler) GetAuditLog(c *gin.Context) {
ctx := context.Background()
// Pagination
limit := 50
offset := 0
if l := c.Query("limit"); l != "" {
if parsed, err := parseIntFromQuery(l); err == nil && parsed > 0 {
limit = parsed
}
}
if o := c.Query("offset"); o != "" {
if parsed, err := parseIntFromQuery(o); err == nil && parsed >= 0 {
offset = parsed
}
}
// Filters
userIDFilter := c.Query("user_id")
actionFilter := c.Query("action")
query := `
SELECT al.id, al.user_id, al.action, al.entity_type, al.entity_id, al.details,
al.ip_address, al.user_agent, al.created_at, u.email
FROM consent_audit_log al
LEFT JOIN users u ON al.user_id = u.id
WHERE 1=1
`
args := []interface{}{}
argCount := 0
if userIDFilter != "" {
argCount++
query += fmt.Sprintf(" AND al.user_id = $%d", argCount)
args = append(args, userIDFilter)
}
if actionFilter != "" {
argCount++
query += fmt.Sprintf(" AND al.action = $%d", argCount)
args = append(args, actionFilter)
}
query += fmt.Sprintf(" ORDER BY al.created_at DESC LIMIT $%d OFFSET $%d", argCount+1, argCount+2)
args = append(args, limit, offset)
rows, err := h.db.Pool.Query(ctx, query, args...)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch audit log"})
return
}
defer rows.Close()
var logs []map[string]interface{}
for rows.Next() {
var (
id uuid.UUID
userIDPtr *uuid.UUID
action string
entityType *string
entityID *uuid.UUID
details *string
ipAddress *string
userAgent *string
createdAt time.Time
email *string
)
if err := rows.Scan(&id, &userIDPtr, &action, &entityType, &entityID, &details,
&ipAddress, &userAgent, &createdAt, &email); err != nil {
continue
}
logs = append(logs, map[string]interface{}{
"id": id,
"user_id": userIDPtr,
"user_email": email,
"action": action,
"entity_type": entityType,
"entity_id": entityID,
"details": details,
"ip_address": ipAddress,
"user_agent": userAgent,
"created_at": createdAt,
})
}
c.JSON(http.StatusOK, gin.H{"audit_log": logs})
}