Files
breakpilot-core/consent-service/internal/handlers/admin_documents.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

392 lines
12 KiB
Go

package handlers
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/breakpilot/consent-service/internal/middleware"
"github.com/breakpilot/consent-service/internal/models"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// ========================================
// ADMIN ENDPOINTS - Document Management
// ========================================
// AdminGetDocuments returns all documents (including inactive) for admin
func (h *Handler) AdminGetDocuments(c *gin.Context) {
ctx := context.Background()
rows, err := h.db.Pool.Query(ctx, `
SELECT id, type, name, description, is_mandatory, is_active, sort_order, created_at, updated_at
FROM legal_documents
ORDER BY sort_order ASC, created_at DESC
`)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch documents"})
return
}
defer rows.Close()
var documents []models.LegalDocument
for rows.Next() {
var doc models.LegalDocument
if err := rows.Scan(&doc.ID, &doc.Type, &doc.Name, &doc.Description,
&doc.IsMandatory, &doc.IsActive, &doc.SortOrder, &doc.CreatedAt, &doc.UpdatedAt); err != nil {
continue
}
documents = append(documents, doc)
}
c.JSON(http.StatusOK, gin.H{"documents": documents})
}
// AdminCreateDocument creates a new legal document
func (h *Handler) AdminCreateDocument(c *gin.Context) {
var req models.CreateDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
ctx := context.Background()
var docID uuid.UUID
err := h.db.Pool.QueryRow(ctx, `
INSERT INTO legal_documents (type, name, description, is_mandatory)
VALUES ($1, $2, $3, $4)
RETURNING id
`, req.Type, req.Name, req.Description, req.IsMandatory).Scan(&docID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create document"})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "Document created successfully",
"id": docID,
})
}
// AdminUpdateDocument updates a legal document
func (h *Handler) AdminUpdateDocument(c *gin.Context) {
docID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
return
}
var req struct {
Name *string `json:"name"`
Description *string `json:"description"`
IsMandatory *bool `json:"is_mandatory"`
IsActive *bool `json:"is_active"`
SortOrder *int `json:"sort_order"`
}
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 legal_documents
SET name = COALESCE($2, name),
description = COALESCE($3, description),
is_mandatory = COALESCE($4, is_mandatory),
is_active = COALESCE($5, is_active),
sort_order = COALESCE($6, sort_order),
updated_at = NOW()
WHERE id = $1
`, docID, req.Name, req.Description, req.IsMandatory, req.IsActive, req.SortOrder)
if err != nil || result.RowsAffected() == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Document not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Document updated successfully"})
}
// AdminDeleteDocument soft-deletes a document (sets is_active to false)
func (h *Handler) AdminDeleteDocument(c *gin.Context) {
docID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
return
}
ctx := context.Background()
result, err := h.db.Pool.Exec(ctx, `
UPDATE legal_documents
SET is_active = false, updated_at = NOW()
WHERE id = $1
`, docID)
if err != nil || result.RowsAffected() == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Document not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Document deleted successfully"})
}
// ========================================
// ADMIN ENDPOINTS - Version Management
// ========================================
// AdminGetVersions returns all versions for a document
func (h *Handler) AdminGetVersions(c *gin.Context) {
docID, err := uuid.Parse(c.Param("docId"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
return
}
ctx := context.Background()
rows, err := h.db.Pool.Query(ctx, `
SELECT id, document_id, version, language, title, content, summary, status,
published_at, scheduled_publish_at, created_by, approved_by, approved_at, created_at, updated_at
FROM document_versions
WHERE document_id = $1
ORDER BY created_at DESC
`, docID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch versions"})
return
}
defer rows.Close()
var versions []models.DocumentVersion
for rows.Next() {
var v models.DocumentVersion
if err := rows.Scan(&v.ID, &v.DocumentID, &v.Version, &v.Language, &v.Title, &v.Content,
&v.Summary, &v.Status, &v.PublishedAt, &v.ScheduledPublishAt, &v.CreatedBy, &v.ApprovedBy, &v.ApprovedAt, &v.CreatedAt, &v.UpdatedAt); err != nil {
continue
}
versions = append(versions, v)
}
c.JSON(http.StatusOK, gin.H{"versions": versions})
}
// AdminCreateVersion creates a new document version
func (h *Handler) AdminCreateVersion(c *gin.Context) {
var req models.CreateVersionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
docID, err := uuid.Parse(req.DocumentID)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid document ID"})
return
}
userID, _ := middleware.GetUserID(c)
ctx := context.Background()
var versionID uuid.UUID
err = h.db.Pool.QueryRow(ctx, `
INSERT INTO document_versions (document_id, version, language, title, content, summary, status, created_by)
VALUES ($1, $2, $3, $4, $5, $6, 'draft', $7)
RETURNING id
`, docID, req.Version, req.Language, req.Title, req.Content, req.Summary, userID).Scan(&versionID)
if err != nil {
// Check for unique constraint violation
errStr := err.Error()
if strings.Contains(errStr, "duplicate key") || strings.Contains(errStr, "unique constraint") {
c.JSON(http.StatusConflict, gin.H{"error": "Eine Version mit dieser Versionsnummer und Sprache existiert bereits für dieses Dokument"})
return
}
// Log the actual error for debugging
fmt.Printf("POST /api/v1/admin/versions ✗ %v\n", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create version: " + errStr})
return
}
c.JSON(http.StatusCreated, gin.H{
"message": "Version created successfully",
"id": versionID,
})
}
// AdminUpdateVersion updates a document version
func (h *Handler) AdminUpdateVersion(c *gin.Context) {
versionID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version ID"})
return
}
var req models.UpdateVersionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
ctx := context.Background()
// Check if version is in draft or review status (only these can be edited)
var status string
err = h.db.Pool.QueryRow(ctx, `SELECT status FROM document_versions WHERE id = $1`, versionID).Scan(&status)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Version not found"})
return
}
if status != "draft" && status != "review" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Only draft or review versions can be edited"})
return
}
result, err := h.db.Pool.Exec(ctx, `
UPDATE document_versions
SET title = COALESCE($2, title),
content = COALESCE($3, content),
summary = COALESCE($4, summary),
status = COALESCE($5, status),
updated_at = NOW()
WHERE id = $1
`, versionID, req.Title, req.Content, req.Summary, req.Status)
if err != nil || result.RowsAffected() == 0 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update version"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Version updated successfully"})
}
// AdminPublishVersion publishes a document version
func (h *Handler) AdminPublishVersion(c *gin.Context) {
versionID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version ID"})
return
}
userID, _ := middleware.GetUserID(c)
ctx := context.Background()
// Check current status
var status string
err = h.db.Pool.QueryRow(ctx, `SELECT status FROM document_versions WHERE id = $1`, versionID).Scan(&status)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Version not found"})
return
}
if status != "approved" && status != "review" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Only approved or review versions can be published"})
return
}
result, err := h.db.Pool.Exec(ctx, `
UPDATE document_versions
SET status = 'published',
published_at = NOW(),
approved_by = $2,
updated_at = NOW()
WHERE id = $1
`, versionID, userID)
if err != nil || result.RowsAffected() == 0 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to publish version"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Version published successfully"})
}
// AdminArchiveVersion archives a document version
func (h *Handler) AdminArchiveVersion(c *gin.Context) {
versionID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version ID"})
return
}
ctx := context.Background()
result, err := h.db.Pool.Exec(ctx, `
UPDATE document_versions
SET status = 'archived', updated_at = NOW()
WHERE id = $1
`, versionID)
if err != nil || result.RowsAffected() == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Version not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Version archived successfully"})
}
// AdminDeleteVersion permanently deletes a draft/rejected version
// Only draft and rejected versions can be deleted. Published versions must be archived.
func (h *Handler) AdminDeleteVersion(c *gin.Context) {
versionID, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version ID"})
return
}
ctx := context.Background()
// First check the version status - only draft/rejected can be deleted
var status string
var version string
var docID uuid.UUID
err = h.db.Pool.QueryRow(ctx, `
SELECT status, version, document_id FROM document_versions WHERE id = $1
`, versionID).Scan(&status, &version, &docID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Version not found"})
return
}
// Only allow deletion of draft and rejected versions
if status != "draft" && status != "rejected" {
c.JSON(http.StatusForbidden, gin.H{
"error": "Cannot delete version",
"message": "Only draft or rejected versions can be deleted. Published versions must be archived instead.",
"status": status,
})
return
}
// Delete the version
result, err := h.db.Pool.Exec(ctx, `
DELETE FROM document_versions WHERE id = $1
`, versionID)
if err != nil || result.RowsAffected() == 0 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete version"})
return
}
// Log the deletion
userID, _ := c.Get("user_id")
h.db.Pool.Exec(ctx, `
INSERT INTO consent_audit_log (action, entity_type, entity_id, user_id, details, ip_address, user_agent)
VALUES ('version_deleted', 'document_version', $1, $2, $3, $4, $5)
`, versionID, userID, "Version "+version+" permanently deleted", c.ClientIP(), c.Request.UserAgent())
c.JSON(http.StatusOK, gin.H{
"message": "Version deleted successfully",
"deleted_version": version,
"version_id": versionID,
})
}