Files
sub2api/backend/internal/service/admin_compliance.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

162 lines
5.5 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
)
const (
AdminComplianceVersion = "v2026.06.10"
AdminComplianceDocumentPathZH = "docs/legal/admin-compliance.zh.md"
AdminComplianceDocumentPathEN = "docs/legal/admin-compliance.en.md"
AdminComplianceDocumentURLZH = "https://github.com/Wei-Shaw/sub2api/blob/main/docs/legal/admin-compliance.zh.md"
AdminComplianceDocumentURLEN = "https://github.com/Wei-Shaw/sub2api/blob/main/docs/legal/admin-compliance.en.md"
AdminComplianceAckPhraseZH = "我已阅读、理解并同意 Sub2API 部署与运营合规承诺"
AdminComplianceAckPhraseEN = "I have read, understood, and agree to the Sub2API Deployment and Operation Compliance Commitment"
settingKeyAdminComplianceAcknowledgement = "admin_compliance_acknowledgement"
)
var (
ErrAdminComplianceAcknowledgementRequired = infraerrors.New(
http.StatusLocked,
"ADMIN_COMPLIANCE_ACK_REQUIRED",
"administrator compliance acknowledgement is required",
)
ErrAdminComplianceInvalidPhrase = infraerrors.BadRequest(
"ADMIN_COMPLIANCE_INVALID_PHRASE",
"confirmation phrase does not match",
)
)
type AdminComplianceAcknowledgement struct {
Version string `json:"version"`
DocumentZH string `json:"document_zh"`
DocumentEN string `json:"document_en"`
AdminUserID int64 `json:"admin_user_id"`
IPAddress string `json:"ip_address,omitempty"`
UserAgent string `json:"user_agent,omitempty"`
AcceptedAt time.Time `json:"accepted_at"`
}
type AdminComplianceStatus struct {
Required bool `json:"required"`
Version string `json:"version"`
DocumentPathZH string `json:"document_path_zh"`
DocumentPathEN string `json:"document_path_en"`
DocumentURLZH string `json:"document_url_zh"`
DocumentURLEN string `json:"document_url_en"`
AckPhraseZH string `json:"ack_phrase_zh"`
AckPhraseEN string `json:"ack_phrase_en"`
Acknowledgement *AdminComplianceAcknowledgement `json:"acknowledgement,omitempty"`
}
type AdminComplianceAcceptInput struct {
AdminUserID int64
Phrase string
Language string
IPAddress string
UserAgent string
}
func normalizeAdminComplianceLanguage(raw string) string {
raw = strings.ToLower(strings.TrimSpace(raw))
if strings.HasPrefix(raw, "zh") {
return "zh"
}
return "en"
}
func expectedAdminCompliancePhrase(language string) string {
if normalizeAdminComplianceLanguage(language) == "zh" {
return AdminComplianceAckPhraseZH
}
return AdminComplianceAckPhraseEN
}
func adminComplianceAcknowledgementKey(adminUserID int64) string {
if adminUserID <= 0 {
return settingKeyAdminComplianceAcknowledgement
}
return settingKeyAdminComplianceAcknowledgement + ":" + strconv.FormatInt(adminUserID, 10)
}
func (s *SettingService) GetAdminComplianceStatus(ctx context.Context, adminUserID int64) (*AdminComplianceStatus, error) {
status := &AdminComplianceStatus{
Required: true,
Version: AdminComplianceVersion,
DocumentPathZH: AdminComplianceDocumentPathZH,
DocumentPathEN: AdminComplianceDocumentPathEN,
DocumentURLZH: AdminComplianceDocumentURLZH,
DocumentURLEN: AdminComplianceDocumentURLEN,
AckPhraseZH: AdminComplianceAckPhraseZH,
AckPhraseEN: AdminComplianceAckPhraseEN,
}
if s == nil || s.settingRepo == nil {
return status, nil
}
raw, err := s.settingRepo.GetValue(ctx, adminComplianceAcknowledgementKey(adminUserID))
if err != nil {
if errors.Is(err, ErrSettingNotFound) {
return status, nil
}
return nil, fmt.Errorf("get admin compliance acknowledgement: %w", err)
}
var ack AdminComplianceAcknowledgement
if err := json.Unmarshal([]byte(raw), &ack); err != nil {
return status, nil
}
if ack.Version == AdminComplianceVersion {
status.Required = false
status.Acknowledgement = &ack
}
return status, nil
}
func (s *SettingService) IsAdminComplianceAcknowledged(ctx context.Context, adminUserID int64) (bool, error) {
status, err := s.GetAdminComplianceStatus(ctx, adminUserID)
if err != nil {
return false, err
}
return status != nil && !status.Required, nil
}
func (s *SettingService) AcceptAdminCompliance(ctx context.Context, input AdminComplianceAcceptInput) (*AdminComplianceStatus, error) {
if s == nil || s.settingRepo == nil {
return nil, infraerrors.InternalServer("SETTING_SERVICE_UNAVAILABLE", "setting service is unavailable")
}
phrase := strings.TrimSpace(input.Phrase)
if phrase != expectedAdminCompliancePhrase(input.Language) {
return nil, ErrAdminComplianceInvalidPhrase
}
ack := AdminComplianceAcknowledgement{
Version: AdminComplianceVersion,
DocumentZH: AdminComplianceDocumentPathZH,
DocumentEN: AdminComplianceDocumentPathEN,
AdminUserID: input.AdminUserID,
IPAddress: strings.TrimSpace(input.IPAddress),
UserAgent: strings.TrimSpace(input.UserAgent),
AcceptedAt: time.Now().UTC(),
}
payload, err := json.Marshal(ack)
if err != nil {
return nil, fmt.Errorf("marshal admin compliance acknowledgement: %w", err)
}
if err := s.settingRepo.Set(ctx, adminComplianceAcknowledgementKey(input.AdminUserID), string(payload)); err != nil {
return nil, fmt.Errorf("save admin compliance acknowledgement: %w", err)
}
return s.GetAdminComplianceStatus(ctx, input.AdminUserID)
}