745cdd79cf
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>
389 lines
10 KiB
Go
389 lines
10 KiB
Go
package transport
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/ops/app"
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
|
)
|
|
|
|
type complianceDecisionRequest struct {
|
|
Reason string `json:"reason" binding:"required"`
|
|
}
|
|
|
|
type complianceMasterSwitchRequest struct {
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
type complianceRollbackRequest struct {
|
|
Version int `json:"version" binding:"required"`
|
|
}
|
|
|
|
func listComplianceDictionariesHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
data, err := svc.ListDictionaries(c.Request.Context())
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func createComplianceDictionaryHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var body app.UpsertComplianceDictionaryRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
data, err := svc.CreateDictionary(c.Request.Context(), body)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func updateComplianceDictionaryHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
var body app.UpsertComplianceDictionaryRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
data, err := svc.UpdateDictionary(c.Request.Context(), id, body)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func deleteComplianceDictionaryHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
if err := svc.DeleteDictionary(c.Request.Context(), id); err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, gin.H{"deleted": true})
|
|
}
|
|
}
|
|
|
|
func listComplianceTermsHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
data, err := svc.ListTerms(c.Request.Context(), id)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func createComplianceTermHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
var body app.CreateComplianceDictionaryTermRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
data, err := svc.CreateTerm(c.Request.Context(), id, body)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func deleteComplianceTermHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
dictID, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
termID, err := parsePositiveInt64(c.Param("term_id"))
|
|
if err != nil {
|
|
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "无效的词条 ID"))
|
|
return
|
|
}
|
|
if err := svc.DeleteTerm(c.Request.Context(), dictID, termID); err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, gin.H{"deleted": true})
|
|
}
|
|
}
|
|
|
|
func batchImportComplianceTermsHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
var body app.BatchImportComplianceTermsRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
data, err := svc.BatchImportTerms(c.Request.Context(), id, body)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func publishComplianceDictionaryHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
data, err := svc.PublishDictionary(c.Request.Context(), id)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func rollbackComplianceDictionaryHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
var body complianceRollbackRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
data, err := svc.RollbackDictionary(c.Request.Context(), id, body.Version)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func getGlobalCompliancePolicyHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
data, err := svc.GetGlobalPolicy(c.Request.Context())
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func updateGlobalCompliancePolicyHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var body app.UpdateCompliancePolicyRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
data, err := svc.UpdateGlobalPolicy(c.Request.Context(), body)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func setGlobalComplianceMasterSwitchHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
var body complianceMasterSwitchRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
data, err := svc.SetGlobalMasterSwitch(c.Request.Context(), body.Enabled)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func listComplianceManualReviewsHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", c.DefaultQuery("size", "50")))
|
|
var tenantID *int64
|
|
if raw := c.Query("tenant_id"); raw != "" {
|
|
id, err := parsePositiveInt64(raw)
|
|
if err != nil {
|
|
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "无效的租户 ID"))
|
|
return
|
|
}
|
|
tenantID = &id
|
|
}
|
|
data, err := svc.ListManualReviews(c.Request.Context(), app.ComplianceManualReviewListRequest{
|
|
Status: c.Query("status"),
|
|
TenantID: tenantID,
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
})
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func getComplianceManualReviewHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
data, err := svc.GetManualReview(c.Request.Context(), id)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func approveComplianceManualReviewHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return requireComplianceReviewPermission(decideComplianceManualReviewHandler(svc, true))
|
|
}
|
|
|
|
func rejectComplianceManualReviewHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return requireComplianceReviewPermission(decideComplianceManualReviewHandler(svc, false))
|
|
}
|
|
|
|
func decideComplianceManualReviewHandler(svc *app.ComplianceService, approve bool) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
id, err := parseIDParam(c)
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
var body complianceDecisionRequest
|
|
if err := c.ShouldBindJSON(&body); err != nil {
|
|
response.Error(c, response.ErrBadRequest(40000, "invalid_payload", err.Error()))
|
|
return
|
|
}
|
|
var data any
|
|
if approve {
|
|
data, err = svc.ApproveManualReview(c.Request.Context(), id, body.Reason)
|
|
} else {
|
|
data, err = svc.RejectManualReview(c.Request.Context(), id, body.Reason)
|
|
}
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func listComplianceRecordsHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", c.DefaultQuery("size", "50")))
|
|
var tenantID *int64
|
|
if raw := c.Query("tenant_id"); raw != "" {
|
|
id, err := parsePositiveInt64(raw)
|
|
if err != nil {
|
|
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "无效的租户 ID"))
|
|
return
|
|
}
|
|
tenantID = &id
|
|
}
|
|
var articleID *int64
|
|
if raw := c.Query("article_id"); raw != "" {
|
|
id, err := parsePositiveInt64(raw)
|
|
if err != nil {
|
|
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "无效的文章 ID"))
|
|
return
|
|
}
|
|
articleID = &id
|
|
}
|
|
data, err := svc.ListRecords(c.Request.Context(), app.ComplianceRecordListRequest{
|
|
TenantID: tenantID,
|
|
ArticleID: articleID,
|
|
Decision: c.Query("decision"),
|
|
Page: page,
|
|
PageSize: pageSize,
|
|
})
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func complianceStatsHandler(svc *app.ComplianceService) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
data, err := svc.Stats(c.Request.Context())
|
|
if err != nil {
|
|
response.Error(c, err)
|
|
return
|
|
}
|
|
response.Success(c, data)
|
|
}
|
|
}
|
|
|
|
func requireComplianceReviewPermission(next gin.HandlerFunc) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
actor := actorFromGin(c)
|
|
if actor == nil || !strings.EqualFold(strings.TrimSpace(actor.Role), "admin") {
|
|
response.Error(c, response.ErrForbidden(40331, "compliance_review_forbidden", "operator is not allowed to review compliance requests"))
|
|
return
|
|
}
|
|
next(c)
|
|
}
|
|
}
|
|
|
|
func parsePositiveInt64(raw string) (int64, error) {
|
|
id, err := strconv.ParseInt(raw, 10, 64)
|
|
if err != nil || id <= 0 {
|
|
return 0, strconv.ErrSyntax
|
|
}
|
|
return id, nil
|
|
}
|