[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>
This commit is contained in:
391
consent-service/internal/handlers/admin_documents.go
Normal file
391
consent-service/internal/handlers/admin_documents.go
Normal file
@@ -0,0 +1,391 @@
|
||||
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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user