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>
245 lines
6.7 KiB
Go
245 lines
6.7 KiB
Go
package handlers
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/breakpilot/consent-service/internal/middleware"
|
|
"github.com/breakpilot/consent-service/internal/models"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ========================================
|
|
// PUBLIC ENDPOINTS - Consent
|
|
// ========================================
|
|
|
|
// CreateConsent creates a new user consent
|
|
func (h *Handler) CreateConsent(c *gin.Context) {
|
|
var req models.CreateConsentRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
|
|
return
|
|
}
|
|
|
|
userID, err := middleware.GetUserID(c)
|
|
if err != nil || userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user"})
|
|
return
|
|
}
|
|
|
|
versionID, err := uuid.Parse(req.VersionID)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid version ID"})
|
|
return
|
|
}
|
|
|
|
ctx := context.Background()
|
|
ipAddress := middleware.GetClientIP(c)
|
|
userAgent := middleware.GetUserAgent(c)
|
|
|
|
// Upsert consent
|
|
var consentID uuid.UUID
|
|
err = h.db.Pool.QueryRow(ctx, `
|
|
INSERT INTO user_consents (user_id, document_version_id, consented, ip_address, user_agent)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
ON CONFLICT (user_id, document_version_id)
|
|
DO UPDATE SET consented = $3, consented_at = NOW(), withdrawn_at = NULL
|
|
RETURNING id
|
|
`, userID, versionID, req.Consented, ipAddress, userAgent).Scan(&consentID)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save consent"})
|
|
return
|
|
}
|
|
|
|
// Log to audit trail
|
|
h.logAudit(ctx, &userID, "consent_given", "document_version", &versionID, nil, ipAddress, userAgent)
|
|
|
|
c.JSON(http.StatusCreated, gin.H{
|
|
"message": "Consent saved successfully",
|
|
"consent_id": consentID,
|
|
})
|
|
}
|
|
|
|
// GetMyConsents returns all consents for the current user
|
|
func (h *Handler) GetMyConsents(c *gin.Context) {
|
|
userID, err := middleware.GetUserID(c)
|
|
if err != nil || userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user"})
|
|
return
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
rows, err := h.db.Pool.Query(ctx, `
|
|
SELECT uc.id, uc.consented, uc.consented_at, uc.withdrawn_at,
|
|
ld.id, ld.type, ld.name, ld.is_mandatory,
|
|
dv.id, dv.version, dv.language, dv.title
|
|
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.user_id = $1
|
|
ORDER BY uc.consented_at DESC
|
|
`, userID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch consents"})
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var consents []map[string]interface{}
|
|
for rows.Next() {
|
|
var (
|
|
consentID uuid.UUID
|
|
consented bool
|
|
consentedAt time.Time
|
|
withdrawnAt *time.Time
|
|
docID uuid.UUID
|
|
docType string
|
|
docName string
|
|
isMandatory bool
|
|
versionID uuid.UUID
|
|
version string
|
|
language string
|
|
title string
|
|
)
|
|
|
|
if err := rows.Scan(&consentID, &consented, &consentedAt, &withdrawnAt,
|
|
&docID, &docType, &docName, &isMandatory,
|
|
&versionID, &version, &language, &title); err != nil {
|
|
continue
|
|
}
|
|
|
|
consents = append(consents, map[string]interface{}{
|
|
"consent_id": consentID,
|
|
"consented": consented,
|
|
"consented_at": consentedAt,
|
|
"withdrawn_at": withdrawnAt,
|
|
"document": map[string]interface{}{
|
|
"id": docID,
|
|
"type": docType,
|
|
"name": docName,
|
|
"is_mandatory": isMandatory,
|
|
},
|
|
"version": map[string]interface{}{
|
|
"id": versionID,
|
|
"version": version,
|
|
"language": language,
|
|
"title": title,
|
|
},
|
|
})
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"consents": consents})
|
|
}
|
|
|
|
// CheckConsent checks if the user has consented to a document
|
|
func (h *Handler) CheckConsent(c *gin.Context) {
|
|
docType := c.Param("documentType")
|
|
language := c.DefaultQuery("language", "de")
|
|
|
|
userID, err := middleware.GetUserID(c)
|
|
if err != nil || userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user"})
|
|
return
|
|
}
|
|
|
|
ctx := context.Background()
|
|
|
|
// Get latest published version
|
|
var latestVersionID uuid.UUID
|
|
var latestVersion string
|
|
err = h.db.Pool.QueryRow(ctx, `
|
|
SELECT dv.id, dv.version
|
|
FROM document_versions dv
|
|
JOIN legal_documents ld ON dv.document_id = ld.id
|
|
WHERE ld.type = $1 AND dv.language = $2 AND dv.status = 'published'
|
|
ORDER BY dv.published_at DESC
|
|
LIMIT 1
|
|
`, docType, language).Scan(&latestVersionID, &latestVersion)
|
|
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, models.ConsentCheckResponse{
|
|
HasConsent: false,
|
|
NeedsUpdate: false,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Check if user has consented to this version
|
|
var consentedVersionID uuid.UUID
|
|
var consentedVersion string
|
|
var consentedAt time.Time
|
|
err = h.db.Pool.QueryRow(ctx, `
|
|
SELECT dv.id, dv.version, uc.consented_at
|
|
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.user_id = $1 AND ld.type = $2 AND uc.consented = true AND uc.withdrawn_at IS NULL
|
|
ORDER BY uc.consented_at DESC
|
|
LIMIT 1
|
|
`, userID, docType).Scan(&consentedVersionID, &consentedVersion, &consentedAt)
|
|
|
|
if err != nil {
|
|
// No consent found
|
|
latestIDStr := latestVersionID.String()
|
|
c.JSON(http.StatusOK, models.ConsentCheckResponse{
|
|
HasConsent: false,
|
|
CurrentVersionID: &latestIDStr,
|
|
NeedsUpdate: true,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Check if consent is for latest version
|
|
needsUpdate := consentedVersionID != latestVersionID
|
|
latestIDStr := latestVersionID.String()
|
|
consentedVerStr := consentedVersion
|
|
|
|
c.JSON(http.StatusOK, models.ConsentCheckResponse{
|
|
HasConsent: true,
|
|
CurrentVersionID: &latestIDStr,
|
|
ConsentedVersion: &consentedVerStr,
|
|
NeedsUpdate: needsUpdate,
|
|
ConsentedAt: &consentedAt,
|
|
})
|
|
}
|
|
|
|
// WithdrawConsent withdraws a consent
|
|
func (h *Handler) WithdrawConsent(c *gin.Context) {
|
|
consentID, err := uuid.Parse(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid consent ID"})
|
|
return
|
|
}
|
|
|
|
userID, err := middleware.GetUserID(c)
|
|
if err != nil || userID == uuid.Nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid user"})
|
|
return
|
|
}
|
|
|
|
ctx := context.Background()
|
|
ipAddress := middleware.GetClientIP(c)
|
|
userAgent := middleware.GetUserAgent(c)
|
|
|
|
// Update consent
|
|
result, err := h.db.Pool.Exec(ctx, `
|
|
UPDATE user_consents
|
|
SET withdrawn_at = NOW(), consented = false
|
|
WHERE id = $1 AND user_id = $2
|
|
`, consentID, userID)
|
|
|
|
if err != nil || result.RowsAffected() == 0 {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "Consent not found"})
|
|
return
|
|
}
|
|
|
|
// Log to audit trail
|
|
h.logAudit(ctx, &userID, "consent_withdrawn", "consent", &consentID, nil, ipAddress, userAgent)
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "Consent withdrawn successfully"})
|
|
}
|