Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
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
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
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user