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}) }