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>
392 lines
12 KiB
Go
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,
|
|
})
|
|
}
|