feat(compliance): add content compliance detection across tenant, ops, and clients
Implements the Revision 8/9 compliance design: tenant + ops backends, ops-web content-safety pages, admin-web editor/publish gate integration, desktop publish block surfacing, and supporting migrations / shared types / config plumbing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/bootstrap"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/response"
|
||||
tenantcompliance "github.com/geo-platform/tenant-api/internal/tenant/compliance"
|
||||
)
|
||||
|
||||
type ComplianceHandler struct {
|
||||
svc *tenantcompliance.Service
|
||||
}
|
||||
|
||||
func NewComplianceHandler(a *bootstrap.App) *ComplianceHandler {
|
||||
return &ComplianceHandler{
|
||||
svc: tenantcompliance.NewService(a.DB, a.ConfigStore, a.Logger).WithRabbitMQ(a.RabbitMQ).WithLLM(a.LLM),
|
||||
}
|
||||
}
|
||||
|
||||
type complianceCheckPlainRequest struct {
|
||||
Title string `json:"title"`
|
||||
Plaintext string `json:"plaintext" binding:"required"`
|
||||
TargetPlatforms []string `json:"target_platforms"`
|
||||
}
|
||||
|
||||
type complianceCheckArticleRequest struct {
|
||||
ArticleVersionID *int64 `json:"article_version_id"`
|
||||
Title *string `json:"title"`
|
||||
Plaintext *string `json:"plaintext"`
|
||||
TargetPlatforms []string `json:"target_platforms"`
|
||||
TriggerSource string `json:"trigger_source"`
|
||||
}
|
||||
|
||||
type complianceCreateAckRequest struct {
|
||||
AcknowledgedViolationIDs []int64 `json:"acknowledged_violation_ids" binding:"required"`
|
||||
}
|
||||
|
||||
type complianceCreateManualReviewRequest struct {
|
||||
CheckRecordID int64 `json:"check_record_id" binding:"required"`
|
||||
Note string `json:"note" binding:"required"`
|
||||
}
|
||||
|
||||
type complianceCancelManualReviewRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
func (h *ComplianceHandler) RuntimeStatus(c *gin.Context) {
|
||||
actor := auth.MustActor(c.Request.Context())
|
||||
data, err := h.svc.RuntimeStatus(c.Request.Context(), actor.TenantID)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *ComplianceHandler) CheckPlain(c *gin.Context) {
|
||||
var req complianceCheckPlainRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
actor := auth.MustActor(c.Request.Context())
|
||||
data, err := h.svc.CheckPlain(c.Request.Context(), tenantcompliance.CheckPlainRequest{
|
||||
TenantID: actor.TenantID,
|
||||
ActorID: actor.UserID,
|
||||
Title: req.Title,
|
||||
Plaintext: req.Plaintext,
|
||||
TargetPlatforms: req.TargetPlatforms,
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *ComplianceHandler) CreateAck(c *gin.Context) {
|
||||
recordID, ok := parseComplianceInt64Param(c, "id", "check record id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req complianceCreateAckRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
actor := auth.MustActor(c.Request.Context())
|
||||
data, err := h.svc.CreateAck(c.Request.Context(), actor.TenantID, recordID, actor.UserID, req.AcknowledgedViolationIDs)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.SuccessWithStatus(c, 201, data)
|
||||
}
|
||||
|
||||
func (h *ComplianceHandler) CheckArticle(c *gin.Context) {
|
||||
articleID, ok := parseComplianceInt64Param(c, "id", "article id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req complianceCheckArticleRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
actor := auth.MustActor(c.Request.Context())
|
||||
data, err := h.svc.CheckArticle(c.Request.Context(), tenantcompliance.CheckArticleRequest{
|
||||
TenantID: actor.TenantID,
|
||||
ArticleID: articleID,
|
||||
ArticleVersionID: req.ArticleVersionID,
|
||||
ActorID: actor.UserID,
|
||||
Title: req.Title,
|
||||
Plaintext: req.Plaintext,
|
||||
TargetPlatforms: req.TargetPlatforms,
|
||||
TriggerSource: req.TriggerSource,
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *ComplianceHandler) LatestCheck(c *gin.Context) {
|
||||
articleID, ok := parseComplianceInt64Param(c, "id", "article id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
actor := auth.MustActor(c.Request.Context())
|
||||
data, err := h.svc.LatestCheck(c.Request.Context(), actor.TenantID, articleID)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *ComplianceHandler) SubmitManualReview(c *gin.Context) {
|
||||
articleID, ok := parseComplianceInt64Param(c, "id", "article id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req complianceCreateManualReviewRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
actor := auth.MustActor(c.Request.Context())
|
||||
data, err := h.svc.SubmitManualReview(c.Request.Context(), tenantcompliance.CreateManualReviewRequest{
|
||||
TenantID: actor.TenantID,
|
||||
ArticleID: articleID,
|
||||
ActorID: actor.UserID,
|
||||
CheckRecordID: req.CheckRecordID,
|
||||
Note: req.Note,
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.SuccessWithStatus(c, 201, data)
|
||||
}
|
||||
|
||||
func (h *ComplianceHandler) LatestManualReview(c *gin.Context) {
|
||||
articleID, ok := parseComplianceInt64Param(c, "id", "article id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
versionID, ok := optionalComplianceInt64Query(c, "article_version_id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
actor := auth.MustActor(c.Request.Context())
|
||||
data, err := h.svc.LatestManualReview(c.Request.Context(), actor.TenantID, articleID, versionID)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *ComplianceHandler) CancelManualReview(c *gin.Context) {
|
||||
articleID, ok := parseComplianceInt64Param(c, "id", "article id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
reviewID, ok := parseComplianceInt64Param(c, "review_id", "manual review id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req complianceCancelManualReviewRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
actor := auth.MustActor(c.Request.Context())
|
||||
data, err := h.svc.CancelManualReview(c.Request.Context(), tenantcompliance.CancelManualReviewRequest{
|
||||
TenantID: actor.TenantID,
|
||||
ArticleID: articleID,
|
||||
ActorID: actor.UserID,
|
||||
ReviewID: reviewID,
|
||||
Reason: req.Reason,
|
||||
})
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func parseComplianceInt64Param(c *gin.Context, name, label string) (int64, bool) {
|
||||
value, err := strconv.ParseInt(strings.TrimSpace(c.Param(name)), 10, 64)
|
||||
if err != nil || value <= 0 {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_id", label+" must be a positive integer"))
|
||||
return 0, false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func optionalComplianceInt64Query(c *gin.Context, name string) (*int64, bool) {
|
||||
raw := strings.TrimSpace(c.Query(name))
|
||||
if raw == "" {
|
||||
return nil, true
|
||||
}
|
||||
value, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil || value <= 0 {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", name+" must be a positive integer"))
|
||||
return nil, false
|
||||
}
|
||||
return &value, true
|
||||
}
|
||||
Reference in New Issue
Block a user