admin_compliance.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package service
  2. import (
  3. "context"
  4. "encoding/json"
  5. "errors"
  6. "fmt"
  7. "net/http"
  8. "strconv"
  9. "strings"
  10. "time"
  11. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  12. )
  13. const (
  14. AdminComplianceVersion = "v2026.06.10"
  15. AdminComplianceDocumentPathZH = "docs/legal/admin-compliance.zh.md"
  16. AdminComplianceDocumentPathEN = "docs/legal/admin-compliance.en.md"
  17. AdminComplianceDocumentURLZH = "https://github.com/Wei-Shaw/sub2api/blob/main/docs/legal/admin-compliance.zh.md"
  18. AdminComplianceDocumentURLEN = "https://github.com/Wei-Shaw/sub2api/blob/main/docs/legal/admin-compliance.en.md"
  19. AdminComplianceAckPhraseZH = "我已阅读、理解并同意 Sub2API 部署与运营合规承诺"
  20. AdminComplianceAckPhraseEN = "I have read, understood, and agree to the Sub2API Deployment and Operation Compliance Commitment"
  21. settingKeyAdminComplianceAcknowledgement = "admin_compliance_acknowledgement"
  22. )
  23. var (
  24. ErrAdminComplianceAcknowledgementRequired = infraerrors.New(
  25. http.StatusLocked,
  26. "ADMIN_COMPLIANCE_ACK_REQUIRED",
  27. "administrator compliance acknowledgement is required",
  28. )
  29. ErrAdminComplianceInvalidPhrase = infraerrors.BadRequest(
  30. "ADMIN_COMPLIANCE_INVALID_PHRASE",
  31. "confirmation phrase does not match",
  32. )
  33. )
  34. type AdminComplianceAcknowledgement struct {
  35. Version string `json:"version"`
  36. DocumentZH string `json:"document_zh"`
  37. DocumentEN string `json:"document_en"`
  38. AdminUserID int64 `json:"admin_user_id"`
  39. IPAddress string `json:"ip_address,omitempty"`
  40. UserAgent string `json:"user_agent,omitempty"`
  41. AcceptedAt time.Time `json:"accepted_at"`
  42. }
  43. type AdminComplianceStatus struct {
  44. Required bool `json:"required"`
  45. Version string `json:"version"`
  46. DocumentPathZH string `json:"document_path_zh"`
  47. DocumentPathEN string `json:"document_path_en"`
  48. DocumentURLZH string `json:"document_url_zh"`
  49. DocumentURLEN string `json:"document_url_en"`
  50. AckPhraseZH string `json:"ack_phrase_zh"`
  51. AckPhraseEN string `json:"ack_phrase_en"`
  52. Acknowledgement *AdminComplianceAcknowledgement `json:"acknowledgement,omitempty"`
  53. }
  54. type AdminComplianceAcceptInput struct {
  55. AdminUserID int64
  56. Phrase string
  57. Language string
  58. IPAddress string
  59. UserAgent string
  60. }
  61. func normalizeAdminComplianceLanguage(raw string) string {
  62. raw = strings.ToLower(strings.TrimSpace(raw))
  63. if strings.HasPrefix(raw, "zh") {
  64. return "zh"
  65. }
  66. return "en"
  67. }
  68. func expectedAdminCompliancePhrase(language string) string {
  69. if normalizeAdminComplianceLanguage(language) == "zh" {
  70. return AdminComplianceAckPhraseZH
  71. }
  72. return AdminComplianceAckPhraseEN
  73. }
  74. func adminComplianceAcknowledgementKey(adminUserID int64) string {
  75. if adminUserID <= 0 {
  76. return settingKeyAdminComplianceAcknowledgement
  77. }
  78. return settingKeyAdminComplianceAcknowledgement + ":" + strconv.FormatInt(adminUserID, 10)
  79. }
  80. func (s *SettingService) GetAdminComplianceStatus(ctx context.Context, adminUserID int64) (*AdminComplianceStatus, error) {
  81. status := &AdminComplianceStatus{
  82. Required: true,
  83. Version: AdminComplianceVersion,
  84. DocumentPathZH: AdminComplianceDocumentPathZH,
  85. DocumentPathEN: AdminComplianceDocumentPathEN,
  86. DocumentURLZH: AdminComplianceDocumentURLZH,
  87. DocumentURLEN: AdminComplianceDocumentURLEN,
  88. AckPhraseZH: AdminComplianceAckPhraseZH,
  89. AckPhraseEN: AdminComplianceAckPhraseEN,
  90. }
  91. if s == nil || s.settingRepo == nil {
  92. return status, nil
  93. }
  94. raw, err := s.settingRepo.GetValue(ctx, adminComplianceAcknowledgementKey(adminUserID))
  95. if err != nil {
  96. if errors.Is(err, ErrSettingNotFound) {
  97. return status, nil
  98. }
  99. return nil, fmt.Errorf("get admin compliance acknowledgement: %w", err)
  100. }
  101. var ack AdminComplianceAcknowledgement
  102. if err := json.Unmarshal([]byte(raw), &ack); err != nil {
  103. return status, nil
  104. }
  105. if ack.Version == AdminComplianceVersion {
  106. status.Required = false
  107. status.Acknowledgement = &ack
  108. }
  109. return status, nil
  110. }
  111. func (s *SettingService) IsAdminComplianceAcknowledged(ctx context.Context, adminUserID int64) (bool, error) {
  112. status, err := s.GetAdminComplianceStatus(ctx, adminUserID)
  113. if err != nil {
  114. return false, err
  115. }
  116. return status != nil && !status.Required, nil
  117. }
  118. func (s *SettingService) AcceptAdminCompliance(ctx context.Context, input AdminComplianceAcceptInput) (*AdminComplianceStatus, error) {
  119. if s == nil || s.settingRepo == nil {
  120. return nil, infraerrors.InternalServer("SETTING_SERVICE_UNAVAILABLE", "setting service is unavailable")
  121. }
  122. phrase := strings.TrimSpace(input.Phrase)
  123. if phrase != expectedAdminCompliancePhrase(input.Language) {
  124. return nil, ErrAdminComplianceInvalidPhrase
  125. }
  126. ack := AdminComplianceAcknowledgement{
  127. Version: AdminComplianceVersion,
  128. DocumentZH: AdminComplianceDocumentPathZH,
  129. DocumentEN: AdminComplianceDocumentPathEN,
  130. AdminUserID: input.AdminUserID,
  131. IPAddress: strings.TrimSpace(input.IPAddress),
  132. UserAgent: strings.TrimSpace(input.UserAgent),
  133. AcceptedAt: time.Now().UTC(),
  134. }
  135. payload, err := json.Marshal(ack)
  136. if err != nil {
  137. return nil, fmt.Errorf("marshal admin compliance acknowledgement: %w", err)
  138. }
  139. if err := s.settingRepo.Set(ctx, adminComplianceAcknowledgementKey(input.AdminUserID), string(payload)); err != nil {
  140. return nil, fmt.Errorf("save admin compliance acknowledgement: %w", err)
  141. }
  142. return s.GetAdminComplianceStatus(ctx, input.AdminUserID)
  143. }