feat: replace Resend email provider with Mailjet
Browse files- backend/internal/handler/admin/setting_handler.go +86 -63
- backend/internal/handler/dto/settings.go +5 -4
- backend/internal/server/routes/admin.go +2 -2
- backend/internal/service/domain_constants.go +91 -163
- backend/internal/service/email_service.go +70 -44
- backend/internal/service/email_service_resend_test.go +71 -41
- backend/internal/service/setting_service.go +18 -12
- backend/internal/service/setting_service_update_test.go +24 -19
- backend/internal/service/settings_view.go +7 -5
- frontend/src/api/admin/settings.ts +25 -21
- frontend/src/i18n/locales/en.ts +20 -15
- frontend/src/i18n/locales/zh.ts +20 -15
- frontend/src/views/admin/SettingsView.vue +85 -55
backend/internal/handler/admin/setting_handler.go
CHANGED
|
@@ -92,9 +92,10 @@ func (h *SettingHandler) GetSettings(c *gin.Context) {
|
|
| 92 |
SMTPFromName: settings.SMTPFromName,
|
| 93 |
SMTPUseTLS: settings.SMTPUseTLS,
|
| 94 |
EmailProvider: settings.EmailProvider,
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
|
|
|
| 98 |
TurnstileEnabled: settings.TurnstileEnabled,
|
| 99 |
TurnstileSiteKey: settings.TurnstileSiteKey,
|
| 100 |
TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
|
|
@@ -155,10 +156,11 @@ type UpdateSettingsRequest struct {
|
|
| 155 |
SMTPFrom string `json:"smtp_from_email"`
|
| 156 |
SMTPFromName string `json:"smtp_from_name"`
|
| 157 |
SMTPUseTLS bool `json:"smtp_use_tls"`
|
| 158 |
-
EmailProvider
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
| 162 |
|
| 163 |
// Cloudflare Turnstile 设置
|
| 164 |
TurnstileEnabled bool `json:"turnstile_enabled"`
|
|
@@ -249,8 +251,8 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|
| 249 |
req.EmailProvider = service.EmailProviderSMTP
|
| 250 |
}
|
| 251 |
}
|
| 252 |
-
req.
|
| 253 |
-
req.
|
| 254 |
req.DefaultSubscriptions = normalizeDefaultSubscriptions(req.DefaultSubscriptions)
|
| 255 |
|
| 256 |
// Turnstile 参数验证
|
|
@@ -494,9 +496,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|
| 494 |
SMTPFromName: req.SMTPFromName,
|
| 495 |
SMTPUseTLS: req.SMTPUseTLS,
|
| 496 |
EmailProvider: req.EmailProvider,
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
|
|
|
| 500 |
TurnstileEnabled: req.TurnstileEnabled,
|
| 501 |
TurnstileSiteKey: req.TurnstileSiteKey,
|
| 502 |
TurnstileSecretKey: req.TurnstileSecretKey,
|
|
@@ -595,9 +598,10 @@ func (h *SettingHandler) UpdateSettings(c *gin.Context) {
|
|
| 595 |
SMTPFromName: updatedSettings.SMTPFromName,
|
| 596 |
SMTPUseTLS: updatedSettings.SMTPUseTLS,
|
| 597 |
EmailProvider: updatedSettings.EmailProvider,
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
|
|
|
| 601 |
TurnstileEnabled: updatedSettings.TurnstileEnabled,
|
| 602 |
TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
|
| 603 |
TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
|
|
@@ -702,14 +706,17 @@ func diffSettings(before *service.SystemSettings, after *service.SystemSettings,
|
|
| 702 |
if before.EmailProvider != after.EmailProvider {
|
| 703 |
changed = append(changed, "email_provider")
|
| 704 |
}
|
| 705 |
-
if req.
|
| 706 |
-
changed = append(changed, "
|
| 707 |
}
|
| 708 |
-
if
|
| 709 |
-
changed = append(changed, "
|
| 710 |
}
|
| 711 |
-
if before.
|
| 712 |
-
changed = append(changed, "
|
|
|
|
|
|
|
|
|
|
| 713 |
}
|
| 714 |
if before.TurnstileEnabled != after.TurnstileEnabled {
|
| 715 |
changed = append(changed, "turnstile_enabled")
|
|
@@ -911,35 +918,43 @@ func (h *SettingHandler) TestSMTPConnection(c *gin.Context) {
|
|
| 911 |
response.Success(c, gin.H{"message": "SMTP connection successful"})
|
| 912 |
}
|
| 913 |
|
| 914 |
-
type
|
| 915 |
-
|
|
|
|
| 916 |
}
|
| 917 |
|
| 918 |
-
//
|
| 919 |
-
// POST /api/v1/admin/settings/test-
|
| 920 |
-
func (h *SettingHandler)
|
| 921 |
-
var req
|
| 922 |
if err := c.ShouldBindJSON(&req); err != nil {
|
| 923 |
response.BadRequest(c, "Invalid request: "+err.Error())
|
| 924 |
return
|
| 925 |
}
|
| 926 |
|
| 927 |
-
apiKey := strings.TrimSpace(req.
|
| 928 |
-
|
| 929 |
-
|
|
|
|
| 930 |
if err == nil && savedConfig != nil {
|
| 931 |
-
apiKey =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 932 |
}
|
| 933 |
}
|
| 934 |
|
| 935 |
-
if err := h.emailService.
|
| 936 |
-
APIKey:
|
|
|
|
| 937 |
}); err != nil {
|
| 938 |
-
response.BadRequest(c, "
|
| 939 |
return
|
| 940 |
}
|
| 941 |
|
| 942 |
-
response.Success(c, gin.H{"message": "
|
| 943 |
}
|
| 944 |
|
| 945 |
// SendTestEmailRequest 发送测试邮件请求
|
|
@@ -1050,56 +1065,64 @@ func (h *SettingHandler) SendTestEmail(c *gin.Context) {
|
|
| 1050 |
response.Success(c, gin.H{"message": "Test email sent successfully"})
|
| 1051 |
}
|
| 1052 |
|
| 1053 |
-
type
|
| 1054 |
-
Email
|
| 1055 |
-
|
| 1056 |
-
|
| 1057 |
-
|
|
|
|
| 1058 |
}
|
| 1059 |
|
| 1060 |
-
//
|
| 1061 |
-
// POST /api/v1/admin/settings/send-test-email-
|
| 1062 |
-
func (h *SettingHandler)
|
| 1063 |
-
var req
|
| 1064 |
if err := c.ShouldBindJSON(&req); err != nil {
|
| 1065 |
response.BadRequest(c, "Invalid request: "+err.Error())
|
| 1066 |
return
|
| 1067 |
}
|
| 1068 |
|
| 1069 |
-
apiKey := strings.TrimSpace(req.
|
| 1070 |
-
|
| 1071 |
-
|
|
|
|
| 1072 |
var err error
|
| 1073 |
-
savedConfig, err = h.emailService.
|
| 1074 |
if err == nil && savedConfig != nil {
|
| 1075 |
-
apiKey =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1076 |
}
|
| 1077 |
}
|
| 1078 |
-
if savedConfig == nil && (strings.TrimSpace(req.
|
| 1079 |
var err error
|
| 1080 |
-
savedConfig, err = h.emailService.
|
| 1081 |
if err != nil {
|
| 1082 |
savedConfig = nil
|
| 1083 |
}
|
| 1084 |
}
|
| 1085 |
|
| 1086 |
-
from := strings.TrimSpace(req.
|
| 1087 |
if from == "" && savedConfig != nil {
|
| 1088 |
from = savedConfig.From
|
| 1089 |
}
|
| 1090 |
-
fromName := strings.TrimSpace(req.
|
| 1091 |
if fromName == "" && savedConfig != nil {
|
| 1092 |
fromName = savedConfig.FromName
|
| 1093 |
}
|
| 1094 |
|
| 1095 |
-
config := &service.
|
| 1096 |
-
APIKey:
|
| 1097 |
-
|
| 1098 |
-
|
|
|
|
| 1099 |
}
|
| 1100 |
|
| 1101 |
siteName := h.settingService.GetSiteName(c.Request.Context())
|
| 1102 |
-
subject := "[" + siteName + "]
|
| 1103 |
body := `
|
| 1104 |
<!DOCTYPE html>
|
| 1105 |
<html>
|
|
@@ -1125,8 +1148,8 @@ func (h *SettingHandler) SendResendTestEmail(c *gin.Context) {
|
|
| 1125 |
<div class="success">✓</div>
|
| 1126 |
-->
|
| 1127 |
<div class="success-ok">OK</div>
|
| 1128 |
-
<h2>
|
| 1129 |
-
<p>This is a test email sent through the
|
| 1130 |
</div>
|
| 1131 |
<div class="footer">
|
| 1132 |
<p>This is an automated test message.</p>
|
|
@@ -1136,12 +1159,12 @@ func (h *SettingHandler) SendResendTestEmail(c *gin.Context) {
|
|
| 1136 |
</html>
|
| 1137 |
`
|
| 1138 |
|
| 1139 |
-
if err := h.emailService.
|
| 1140 |
-
response.BadRequest(c, "Failed to send
|
| 1141 |
return
|
| 1142 |
}
|
| 1143 |
|
| 1144 |
-
response.Success(c, gin.H{"message": "
|
| 1145 |
}
|
| 1146 |
|
| 1147 |
// GetAdminAPIKey 获取管理员 API Key 状态
|
|
|
|
| 92 |
SMTPFromName: settings.SMTPFromName,
|
| 93 |
SMTPUseTLS: settings.SMTPUseTLS,
|
| 94 |
EmailProvider: settings.EmailProvider,
|
| 95 |
+
MailjetAPIKeyConfigured: settings.MailjetAPIKeyConfigured,
|
| 96 |
+
MailjetSecretKeyConfigured: settings.MailjetSecretKeyConfigured,
|
| 97 |
+
MailjetFrom: settings.MailjetFrom,
|
| 98 |
+
MailjetFromName: settings.MailjetFromName,
|
| 99 |
TurnstileEnabled: settings.TurnstileEnabled,
|
| 100 |
TurnstileSiteKey: settings.TurnstileSiteKey,
|
| 101 |
TurnstileSecretKeyConfigured: settings.TurnstileSecretKeyConfigured,
|
|
|
|
| 156 |
SMTPFrom string `json:"smtp_from_email"`
|
| 157 |
SMTPFromName string `json:"smtp_from_name"`
|
| 158 |
SMTPUseTLS bool `json:"smtp_use_tls"`
|
| 159 |
+
EmailProvider string `json:"email_provider" binding:"omitempty,oneof=smtp mailjet"`
|
| 160 |
+
MailjetAPIKey string `json:"mailjet_api_key"`
|
| 161 |
+
MailjetSecretKey string `json:"mailjet_secret_key"`
|
| 162 |
+
MailjetFrom string `json:"mailjet_from_email"`
|
| 163 |
+
MailjetFromName string `json:"mailjet_from_name"`
|
| 164 |
|
| 165 |
// Cloudflare Turnstile 设置
|
| 166 |
TurnstileEnabled bool `json:"turnstile_enabled"`
|
|
|
|
| 251 |
req.EmailProvider = service.EmailProviderSMTP
|
| 252 |
}
|
| 253 |
}
|
| 254 |
+
req.MailjetFrom = strings.TrimSpace(req.MailjetFrom)
|
| 255 |
+
req.MailjetFromName = strings.TrimSpace(req.MailjetFromName)
|
| 256 |
req.DefaultSubscriptions = normalizeDefaultSubscriptions(req.DefaultSubscriptions)
|
| 257 |
|
| 258 |
// Turnstile 参数验证
|
|
|
|
| 496 |
SMTPFromName: req.SMTPFromName,
|
| 497 |
SMTPUseTLS: req.SMTPUseTLS,
|
| 498 |
EmailProvider: req.EmailProvider,
|
| 499 |
+
MailjetAPIKey: req.MailjetAPIKey,
|
| 500 |
+
MailjetSecretKey: req.MailjetSecretKey,
|
| 501 |
+
MailjetFrom: req.MailjetFrom,
|
| 502 |
+
MailjetFromName: req.MailjetFromName,
|
| 503 |
TurnstileEnabled: req.TurnstileEnabled,
|
| 504 |
TurnstileSiteKey: req.TurnstileSiteKey,
|
| 505 |
TurnstileSecretKey: req.TurnstileSecretKey,
|
|
|
|
| 598 |
SMTPFromName: updatedSettings.SMTPFromName,
|
| 599 |
SMTPUseTLS: updatedSettings.SMTPUseTLS,
|
| 600 |
EmailProvider: updatedSettings.EmailProvider,
|
| 601 |
+
MailjetAPIKeyConfigured: updatedSettings.MailjetAPIKeyConfigured,
|
| 602 |
+
MailjetSecretKeyConfigured: updatedSettings.MailjetSecretKeyConfigured,
|
| 603 |
+
MailjetFrom: updatedSettings.MailjetFrom,
|
| 604 |
+
MailjetFromName: updatedSettings.MailjetFromName,
|
| 605 |
TurnstileEnabled: updatedSettings.TurnstileEnabled,
|
| 606 |
TurnstileSiteKey: updatedSettings.TurnstileSiteKey,
|
| 607 |
TurnstileSecretKeyConfigured: updatedSettings.TurnstileSecretKeyConfigured,
|
|
|
|
| 706 |
if before.EmailProvider != after.EmailProvider {
|
| 707 |
changed = append(changed, "email_provider")
|
| 708 |
}
|
| 709 |
+
if req.MailjetAPIKey != "" {
|
| 710 |
+
changed = append(changed, "mailjet_api_key")
|
| 711 |
}
|
| 712 |
+
if req.MailjetSecretKey != "" {
|
| 713 |
+
changed = append(changed, "mailjet_secret_key")
|
| 714 |
}
|
| 715 |
+
if before.MailjetFrom != after.MailjetFrom {
|
| 716 |
+
changed = append(changed, "mailjet_from_email")
|
| 717 |
+
}
|
| 718 |
+
if before.MailjetFromName != after.MailjetFromName {
|
| 719 |
+
changed = append(changed, "mailjet_from_name")
|
| 720 |
}
|
| 721 |
if before.TurnstileEnabled != after.TurnstileEnabled {
|
| 722 |
changed = append(changed, "turnstile_enabled")
|
|
|
|
| 918 |
response.Success(c, gin.H{"message": "SMTP connection successful"})
|
| 919 |
}
|
| 920 |
|
| 921 |
+
type TestMailjetRequest struct {
|
| 922 |
+
MailjetAPIKey string `json:"mailjet_api_key"`
|
| 923 |
+
MailjetSecretKey string `json:"mailjet_secret_key"`
|
| 924 |
}
|
| 925 |
|
| 926 |
+
// TestMailjetConnection tests Mailjet API connectivity.
|
| 927 |
+
// POST /api/v1/admin/settings/test-mailjet
|
| 928 |
+
func (h *SettingHandler) TestMailjetConnection(c *gin.Context) {
|
| 929 |
+
var req TestMailjetRequest
|
| 930 |
if err := c.ShouldBindJSON(&req); err != nil {
|
| 931 |
response.BadRequest(c, "Invalid request: "+err.Error())
|
| 932 |
return
|
| 933 |
}
|
| 934 |
|
| 935 |
+
apiKey := strings.TrimSpace(req.MailjetAPIKey)
|
| 936 |
+
secretKey := strings.TrimSpace(req.MailjetSecretKey)
|
| 937 |
+
if apiKey == "" || secretKey == "" {
|
| 938 |
+
savedConfig, err := h.emailService.GetMailjetConfig(c.Request.Context())
|
| 939 |
if err == nil && savedConfig != nil {
|
| 940 |
+
if apiKey == "" {
|
| 941 |
+
apiKey = savedConfig.APIKey
|
| 942 |
+
}
|
| 943 |
+
if secretKey == "" {
|
| 944 |
+
secretKey = savedConfig.SecretKey
|
| 945 |
+
}
|
| 946 |
}
|
| 947 |
}
|
| 948 |
|
| 949 |
+
if err := h.emailService.TestMailjetConnectionWithConfig(c.Request.Context(), &service.MailjetConfig{
|
| 950 |
+
APIKey: apiKey,
|
| 951 |
+
SecretKey: secretKey,
|
| 952 |
}); err != nil {
|
| 953 |
+
response.BadRequest(c, "Mailjet connection test failed: "+err.Error())
|
| 954 |
return
|
| 955 |
}
|
| 956 |
|
| 957 |
+
response.Success(c, gin.H{"message": "Mailjet connection successful"})
|
| 958 |
}
|
| 959 |
|
| 960 |
// SendTestEmailRequest 发送测试邮件请求
|
|
|
|
| 1065 |
response.Success(c, gin.H{"message": "Test email sent successfully"})
|
| 1066 |
}
|
| 1067 |
|
| 1068 |
+
type SendMailjetTestEmailRequest struct {
|
| 1069 |
+
Email string `json:"email" binding:"required,email"`
|
| 1070 |
+
MailjetAPIKey string `json:"mailjet_api_key"`
|
| 1071 |
+
MailjetSecretKey string `json:"mailjet_secret_key"`
|
| 1072 |
+
MailjetFrom string `json:"mailjet_from_email" binding:"omitempty,email"`
|
| 1073 |
+
MailjetFromName string `json:"mailjet_from_name"`
|
| 1074 |
}
|
| 1075 |
|
| 1076 |
+
// SendMailjetTestEmail sends a test email using Mailjet.
|
| 1077 |
+
// POST /api/v1/admin/settings/send-test-email-mailjet
|
| 1078 |
+
func (h *SettingHandler) SendMailjetTestEmail(c *gin.Context) {
|
| 1079 |
+
var req SendMailjetTestEmailRequest
|
| 1080 |
if err := c.ShouldBindJSON(&req); err != nil {
|
| 1081 |
response.BadRequest(c, "Invalid request: "+err.Error())
|
| 1082 |
return
|
| 1083 |
}
|
| 1084 |
|
| 1085 |
+
apiKey := strings.TrimSpace(req.MailjetAPIKey)
|
| 1086 |
+
secretKey := strings.TrimSpace(req.MailjetSecretKey)
|
| 1087 |
+
var savedConfig *service.MailjetConfig
|
| 1088 |
+
if apiKey == "" || secretKey == "" {
|
| 1089 |
var err error
|
| 1090 |
+
savedConfig, err = h.emailService.GetMailjetConfig(c.Request.Context())
|
| 1091 |
if err == nil && savedConfig != nil {
|
| 1092 |
+
if apiKey == "" {
|
| 1093 |
+
apiKey = savedConfig.APIKey
|
| 1094 |
+
}
|
| 1095 |
+
if secretKey == "" {
|
| 1096 |
+
secretKey = savedConfig.SecretKey
|
| 1097 |
+
}
|
| 1098 |
}
|
| 1099 |
}
|
| 1100 |
+
if savedConfig == nil && (strings.TrimSpace(req.MailjetFrom) == "" || strings.TrimSpace(req.MailjetFromName) == "") {
|
| 1101 |
var err error
|
| 1102 |
+
savedConfig, err = h.emailService.GetMailjetConfig(c.Request.Context())
|
| 1103 |
if err != nil {
|
| 1104 |
savedConfig = nil
|
| 1105 |
}
|
| 1106 |
}
|
| 1107 |
|
| 1108 |
+
from := strings.TrimSpace(req.MailjetFrom)
|
| 1109 |
if from == "" && savedConfig != nil {
|
| 1110 |
from = savedConfig.From
|
| 1111 |
}
|
| 1112 |
+
fromName := strings.TrimSpace(req.MailjetFromName)
|
| 1113 |
if fromName == "" && savedConfig != nil {
|
| 1114 |
fromName = savedConfig.FromName
|
| 1115 |
}
|
| 1116 |
|
| 1117 |
+
config := &service.MailjetConfig{
|
| 1118 |
+
APIKey: apiKey,
|
| 1119 |
+
SecretKey: secretKey,
|
| 1120 |
+
From: from,
|
| 1121 |
+
FromName: fromName,
|
| 1122 |
}
|
| 1123 |
|
| 1124 |
siteName := h.settingService.GetSiteName(c.Request.Context())
|
| 1125 |
+
subject := "[" + siteName + "] Mailjet Test Email"
|
| 1126 |
body := `
|
| 1127 |
<!DOCTYPE html>
|
| 1128 |
<html>
|
|
|
|
| 1148 |
<div class="success">✓</div>
|
| 1149 |
-->
|
| 1150 |
<div class="success-ok">OK</div>
|
| 1151 |
+
<h2>Mailjet API Configuration Successful!</h2>
|
| 1152 |
+
<p>This is a test email sent through the Mailjet API.</p>
|
| 1153 |
</div>
|
| 1154 |
<div class="footer">
|
| 1155 |
<p>This is an automated test message.</p>
|
|
|
|
| 1159 |
</html>
|
| 1160 |
`
|
| 1161 |
|
| 1162 |
+
if err := h.emailService.SendEmailWithMailjetConfig(c.Request.Context(), config, req.Email, subject, body); err != nil {
|
| 1163 |
+
response.BadRequest(c, "Failed to send Mailjet test email: "+err.Error())
|
| 1164 |
return
|
| 1165 |
}
|
| 1166 |
|
| 1167 |
+
response.Success(c, gin.H{"message": "Mailjet test email sent successfully"})
|
| 1168 |
}
|
| 1169 |
|
| 1170 |
// GetAdminAPIKey 获取管理员 API Key 状态
|
backend/internal/handler/dto/settings.go
CHANGED
|
@@ -34,10 +34,11 @@ type SystemSettings struct {
|
|
| 34 |
SMTPFrom string `json:"smtp_from_email"`
|
| 35 |
SMTPFromName string `json:"smtp_from_name"`
|
| 36 |
SMTPUseTLS bool `json:"smtp_use_tls"`
|
| 37 |
-
EmailProvider
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
| 41 |
|
| 42 |
TurnstileEnabled bool `json:"turnstile_enabled"`
|
| 43 |
TurnstileSiteKey string `json:"turnstile_site_key"`
|
|
|
|
| 34 |
SMTPFrom string `json:"smtp_from_email"`
|
| 35 |
SMTPFromName string `json:"smtp_from_name"`
|
| 36 |
SMTPUseTLS bool `json:"smtp_use_tls"`
|
| 37 |
+
EmailProvider string `json:"email_provider"`
|
| 38 |
+
MailjetAPIKeyConfigured bool `json:"mailjet_api_key_configured"`
|
| 39 |
+
MailjetSecretKeyConfigured bool `json:"mailjet_secret_key_configured"`
|
| 40 |
+
MailjetFrom string `json:"mailjet_from_email"`
|
| 41 |
+
MailjetFromName string `json:"mailjet_from_name"`
|
| 42 |
|
| 43 |
TurnstileEnabled bool `json:"turnstile_enabled"`
|
| 44 |
TurnstileSiteKey string `json:"turnstile_site_key"`
|
backend/internal/server/routes/admin.go
CHANGED
|
@@ -398,9 +398,9 @@ func registerSettingsRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|
| 398 |
adminSettings.GET("", h.Admin.Setting.GetSettings)
|
| 399 |
adminSettings.PUT("", h.Admin.Setting.UpdateSettings)
|
| 400 |
adminSettings.POST("/test-smtp", h.Admin.Setting.TestSMTPConnection)
|
| 401 |
-
adminSettings.POST("/test-
|
| 402 |
adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail)
|
| 403 |
-
adminSettings.POST("/send-test-email-
|
| 404 |
// Admin API Key 管理
|
| 405 |
adminSettings.GET("/admin-api-key", h.Admin.Setting.GetAdminAPIKey)
|
| 406 |
adminSettings.POST("/admin-api-key/regenerate", h.Admin.Setting.RegenerateAdminAPIKey)
|
|
|
|
| 398 |
adminSettings.GET("", h.Admin.Setting.GetSettings)
|
| 399 |
adminSettings.PUT("", h.Admin.Setting.UpdateSettings)
|
| 400 |
adminSettings.POST("/test-smtp", h.Admin.Setting.TestSMTPConnection)
|
| 401 |
+
adminSettings.POST("/test-mailjet", h.Admin.Setting.TestMailjetConnection)
|
| 402 |
adminSettings.POST("/send-test-email", h.Admin.Setting.SendTestEmail)
|
| 403 |
+
adminSettings.POST("/send-test-email-mailjet", h.Admin.Setting.SendMailjetTestEmail)
|
| 404 |
// Admin API Key 管理
|
| 405 |
adminSettings.GET("/admin-api-key", h.Admin.Setting.GetAdminAPIKey)
|
| 406 |
adminSettings.POST("/admin-api-key/regenerate", h.Admin.Setting.RegenerateAdminAPIKey)
|
backend/internal/service/domain_constants.go
CHANGED
|
@@ -2,7 +2,7 @@ package service
|
|
| 2 |
|
| 3 |
import "github.com/Wei-Shaw/sub2api/internal/domain"
|
| 4 |
|
| 5 |
-
// Status constants
|
| 6 |
const (
|
| 7 |
StatusActive = domain.StatusActive
|
| 8 |
StatusDisabled = domain.StatusDisabled
|
|
@@ -12,13 +12,13 @@ const (
|
|
| 12 |
StatusExpired = domain.StatusExpired
|
| 13 |
)
|
| 14 |
|
| 15 |
-
// Role constants
|
| 16 |
const (
|
| 17 |
RoleAdmin = domain.RoleAdmin
|
| 18 |
RoleUser = domain.RoleUser
|
| 19 |
)
|
| 20 |
|
| 21 |
-
// Platform constants
|
| 22 |
const (
|
| 23 |
PlatformAnthropic = domain.PlatformAnthropic
|
| 24 |
PlatformOpenAI = domain.PlatformOpenAI
|
|
@@ -27,16 +27,16 @@ const (
|
|
| 27 |
PlatformSora = domain.PlatformSora
|
| 28 |
)
|
| 29 |
|
| 30 |
-
// Account type constants
|
| 31 |
const (
|
| 32 |
-
AccountTypeOAuth = domain.AccountTypeOAuth
|
| 33 |
-
AccountTypeSetupToken = domain.AccountTypeSetupToken
|
| 34 |
-
AccountTypeAPIKey = domain.AccountTypeAPIKey
|
| 35 |
-
AccountTypeUpstream = domain.AccountTypeUpstream
|
| 36 |
-
AccountTypeBedrock = domain.AccountTypeBedrock
|
| 37 |
)
|
| 38 |
|
| 39 |
-
// Redeem type constants
|
| 40 |
const (
|
| 41 |
RedeemTypeBalance = domain.RedeemTypeBalance
|
| 42 |
RedeemTypeConcurrency = domain.RedeemTypeConcurrency
|
|
@@ -44,201 +44,129 @@ const (
|
|
| 44 |
RedeemTypeInvitation = domain.RedeemTypeInvitation
|
| 45 |
)
|
| 46 |
|
| 47 |
-
//
|
| 48 |
const (
|
| 49 |
PromoCodeStatusActive = domain.PromoCodeStatusActive
|
| 50 |
PromoCodeStatusDisabled = domain.PromoCodeStatusDisabled
|
| 51 |
)
|
| 52 |
|
| 53 |
-
// Admin adjustment type constants
|
| 54 |
const (
|
| 55 |
-
AdjustmentTypeAdminBalance = domain.AdjustmentTypeAdminBalance
|
| 56 |
-
AdjustmentTypeAdminConcurrency = domain.AdjustmentTypeAdminConcurrency
|
| 57 |
)
|
| 58 |
|
| 59 |
-
// Group subscription type constants
|
| 60 |
const (
|
| 61 |
-
SubscriptionTypeStandard = domain.SubscriptionTypeStandard
|
| 62 |
-
SubscriptionTypeSubscription = domain.SubscriptionTypeSubscription
|
| 63 |
)
|
| 64 |
|
| 65 |
-
// Subscription status constants
|
| 66 |
const (
|
| 67 |
SubscriptionStatusActive = domain.SubscriptionStatusActive
|
| 68 |
SubscriptionStatusExpired = domain.SubscriptionStatusExpired
|
| 69 |
SubscriptionStatusSuspended = domain.SubscriptionStatusSuspended
|
| 70 |
)
|
| 71 |
|
| 72 |
-
// LinuxDoConnectSyntheticEmailDomain 是 LinuxDo Connect 用户的合成邮箱后缀(RFC 保留域名)。
|
| 73 |
const LinuxDoConnectSyntheticEmailDomain = "@linuxdo-connect.invalid"
|
| 74 |
|
| 75 |
-
// Setting keys
|
| 76 |
const (
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
SettingKeyTotpEnabled = "totp_enabled" // 是否启用 TOTP 2FA 功能
|
| 106 |
-
|
| 107 |
-
// LinuxDo Connect OAuth 登录设置
|
| 108 |
SettingKeyLinuxDoConnectEnabled = "linuxdo_connect_enabled"
|
| 109 |
SettingKeyLinuxDoConnectClientID = "linuxdo_connect_client_id"
|
| 110 |
SettingKeyLinuxDoConnectClientSecret = "linuxdo_connect_client_secret"
|
| 111 |
SettingKeyLinuxDoConnectRedirectURL = "linuxdo_connect_redirect_url"
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
// 管理员 API Key
|
| 133 |
-
SettingKeyAdminAPIKey = "admin_api_key" // 全局管理员 API Key(用于外部系统集成)
|
| 134 |
-
|
| 135 |
-
// Gemini 配额策略(JSON)
|
| 136 |
-
SettingKeyGeminiQuotaPolicy = "gemini_quota_policy"
|
| 137 |
-
|
| 138 |
-
// Model fallback settings
|
| 139 |
SettingKeyEnableModelFallback = "enable_model_fallback"
|
| 140 |
SettingKeyFallbackModelAnthropic = "fallback_model_anthropic"
|
| 141 |
SettingKeyFallbackModelOpenAI = "fallback_model_openai"
|
| 142 |
SettingKeyFallbackModelGemini = "fallback_model_gemini"
|
| 143 |
SettingKeyFallbackModelAntigravity = "fallback_model_antigravity"
|
| 144 |
|
| 145 |
-
// Request identity patch (Claude -> Gemini systemInstruction injection)
|
| 146 |
SettingKeyEnableIdentityPatch = "enable_identity_patch"
|
| 147 |
SettingKeyIdentityPatchPrompt = "identity_patch_prompt"
|
| 148 |
|
| 149 |
-
|
| 150 |
-
// Ops Monitoring (vNext)
|
| 151 |
-
// =========================
|
| 152 |
-
|
| 153 |
-
// SettingKeyOpsMonitoringEnabled is a DB-backed soft switch to enable/disable ops module at runtime.
|
| 154 |
-
SettingKeyOpsMonitoringEnabled = "ops_monitoring_enabled"
|
| 155 |
-
|
| 156 |
-
// SettingKeyOpsRealtimeMonitoringEnabled controls realtime features (e.g. WS/QPS push).
|
| 157 |
SettingKeyOpsRealtimeMonitoringEnabled = "ops_realtime_monitoring_enabled"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
// SettingKeyOpsQueryModeDefault controls the default query mode for ops dashboard (auto/raw/preagg).
|
| 160 |
-
SettingKeyOpsQueryModeDefault = "ops_query_mode_default"
|
| 161 |
-
|
| 162 |
-
// SettingKeyOpsEmailNotificationConfig stores JSON config for ops email notifications.
|
| 163 |
-
SettingKeyOpsEmailNotificationConfig = "ops_email_notification_config"
|
| 164 |
-
|
| 165 |
-
// SettingKeyOpsAlertRuntimeSettings stores JSON config for ops alert evaluator runtime settings.
|
| 166 |
-
SettingKeyOpsAlertRuntimeSettings = "ops_alert_runtime_settings"
|
| 167 |
-
|
| 168 |
-
// SettingKeyOpsMetricsIntervalSeconds controls the ops metrics collector interval (>=60).
|
| 169 |
-
SettingKeyOpsMetricsIntervalSeconds = "ops_metrics_interval_seconds"
|
| 170 |
-
|
| 171 |
-
// SettingKeyOpsAdvancedSettings stores JSON config for ops advanced settings (data retention, aggregation).
|
| 172 |
-
SettingKeyOpsAdvancedSettings = "ops_advanced_settings"
|
| 173 |
-
|
| 174 |
-
// SettingKeyOpsRuntimeLogConfig stores JSON config for runtime log settings.
|
| 175 |
-
SettingKeyOpsRuntimeLogConfig = "ops_runtime_log_config"
|
| 176 |
-
|
| 177 |
-
// =========================
|
| 178 |
-
// Overload Cooldown (529)
|
| 179 |
-
// =========================
|
| 180 |
-
|
| 181 |
-
// SettingKeyOverloadCooldownSettings stores JSON config for 529 overload cooldown handling.
|
| 182 |
SettingKeyOverloadCooldownSettings = "overload_cooldown_settings"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
-
// =========================
|
| 185 |
-
// Stream Timeout Handling
|
| 186 |
-
// =========================
|
| 187 |
-
|
| 188 |
-
// SettingKeyStreamTimeoutSettings stores JSON config for stream timeout handling.
|
| 189 |
-
SettingKeyStreamTimeoutSettings = "stream_timeout_settings"
|
| 190 |
-
|
| 191 |
-
// =========================
|
| 192 |
-
// Request Rectifier (请求整流器)
|
| 193 |
-
// =========================
|
| 194 |
-
|
| 195 |
-
// SettingKeyRectifierSettings stores JSON config for rectifier settings (thinking signature + budget).
|
| 196 |
-
SettingKeyRectifierSettings = "rectifier_settings"
|
| 197 |
-
|
| 198 |
-
// =========================
|
| 199 |
-
// Beta Policy Settings
|
| 200 |
-
// =========================
|
| 201 |
-
|
| 202 |
-
// SettingKeyBetaPolicySettings stores JSON config for beta policy rules.
|
| 203 |
-
SettingKeyBetaPolicySettings = "beta_policy_settings"
|
| 204 |
-
|
| 205 |
-
// =========================
|
| 206 |
-
// Sora S3 存储配置
|
| 207 |
-
// =========================
|
| 208 |
-
|
| 209 |
-
SettingKeySoraS3Enabled = "sora_s3_enabled" // 是否启用 Sora S3 存储
|
| 210 |
-
SettingKeySoraS3Endpoint = "sora_s3_endpoint" // S3 端点地址
|
| 211 |
-
SettingKeySoraS3Region = "sora_s3_region" // S3 区域
|
| 212 |
-
SettingKeySoraS3Bucket = "sora_s3_bucket" // S3 存储桶名称
|
| 213 |
-
SettingKeySoraS3AccessKeyID = "sora_s3_access_key_id" // S3 Access Key ID
|
| 214 |
-
SettingKeySoraS3SecretAccessKey = "sora_s3_secret_access_key" // S3 Secret Access Key(加密存储)
|
| 215 |
-
SettingKeySoraS3Prefix = "sora_s3_prefix" // S3 对象键前缀
|
| 216 |
-
SettingKeySoraS3ForcePathStyle = "sora_s3_force_path_style" // 是否强制 Path Style(兼容 MinIO 等)
|
| 217 |
-
SettingKeySoraS3CDNURL = "sora_s3_cdn_url" // CDN 加速 URL(可选)
|
| 218 |
-
SettingKeySoraS3Profiles = "sora_s3_profiles" // Sora S3 多配置(JSON)
|
| 219 |
-
|
| 220 |
-
// =========================
|
| 221 |
-
// Sora 用户存储配额
|
| 222 |
-
// =========================
|
| 223 |
-
|
| 224 |
-
SettingKeySoraDefaultStorageQuotaBytes = "sora_default_storage_quota_bytes" // 新用户默认 Sora 存储配额(字节)
|
| 225 |
-
|
| 226 |
-
// =========================
|
| 227 |
-
// Claude Code Version Check
|
| 228 |
-
// =========================
|
| 229 |
-
|
| 230 |
-
// SettingKeyMinClaudeCodeVersion 最低 Claude Code 版本号要求 (semver, 如 "2.1.0",空值=不检查)
|
| 231 |
SettingKeyMinClaudeCodeVersion = "min_claude_code_version"
|
| 232 |
-
|
| 233 |
-
// SettingKeyMaxClaudeCodeVersion 最高 Claude Code 版本号限制 (semver, 如 "3.0.0",空值=不检查)
|
| 234 |
SettingKeyMaxClaudeCodeVersion = "max_claude_code_version"
|
| 235 |
|
| 236 |
-
// SettingKeyAllowUngroupedKeyScheduling 允许未分组 API Key 调度(默认 false:未分组 Key 返回 403)
|
| 237 |
SettingKeyAllowUngroupedKeyScheduling = "allow_ungrouped_key_scheduling"
|
| 238 |
-
|
| 239 |
-
// SettingKeyBackendModeEnabled Backend 模式:禁用用户注册和自助服务,仅管理员可登录
|
| 240 |
-
SettingKeyBackendModeEnabled = "backend_mode_enabled"
|
| 241 |
)
|
| 242 |
|
| 243 |
-
// AdminAPIKeyPrefix is the prefix for admin API keys
|
| 244 |
const AdminAPIKeyPrefix = "admin-"
|
|
|
|
| 2 |
|
| 3 |
import "github.com/Wei-Shaw/sub2api/internal/domain"
|
| 4 |
|
| 5 |
+
// Status constants.
|
| 6 |
const (
|
| 7 |
StatusActive = domain.StatusActive
|
| 8 |
StatusDisabled = domain.StatusDisabled
|
|
|
|
| 12 |
StatusExpired = domain.StatusExpired
|
| 13 |
)
|
| 14 |
|
| 15 |
+
// Role constants.
|
| 16 |
const (
|
| 17 |
RoleAdmin = domain.RoleAdmin
|
| 18 |
RoleUser = domain.RoleUser
|
| 19 |
)
|
| 20 |
|
| 21 |
+
// Platform constants.
|
| 22 |
const (
|
| 23 |
PlatformAnthropic = domain.PlatformAnthropic
|
| 24 |
PlatformOpenAI = domain.PlatformOpenAI
|
|
|
|
| 27 |
PlatformSora = domain.PlatformSora
|
| 28 |
)
|
| 29 |
|
| 30 |
+
// Account type constants.
|
| 31 |
const (
|
| 32 |
+
AccountTypeOAuth = domain.AccountTypeOAuth
|
| 33 |
+
AccountTypeSetupToken = domain.AccountTypeSetupToken
|
| 34 |
+
AccountTypeAPIKey = domain.AccountTypeAPIKey
|
| 35 |
+
AccountTypeUpstream = domain.AccountTypeUpstream
|
| 36 |
+
AccountTypeBedrock = domain.AccountTypeBedrock
|
| 37 |
)
|
| 38 |
|
| 39 |
+
// Redeem type constants.
|
| 40 |
const (
|
| 41 |
RedeemTypeBalance = domain.RedeemTypeBalance
|
| 42 |
RedeemTypeConcurrency = domain.RedeemTypeConcurrency
|
|
|
|
| 44 |
RedeemTypeInvitation = domain.RedeemTypeInvitation
|
| 45 |
)
|
| 46 |
|
| 47 |
+
// Promo code status constants.
|
| 48 |
const (
|
| 49 |
PromoCodeStatusActive = domain.PromoCodeStatusActive
|
| 50 |
PromoCodeStatusDisabled = domain.PromoCodeStatusDisabled
|
| 51 |
)
|
| 52 |
|
| 53 |
+
// Admin adjustment type constants.
|
| 54 |
const (
|
| 55 |
+
AdjustmentTypeAdminBalance = domain.AdjustmentTypeAdminBalance
|
| 56 |
+
AdjustmentTypeAdminConcurrency = domain.AdjustmentTypeAdminConcurrency
|
| 57 |
)
|
| 58 |
|
| 59 |
+
// Group subscription type constants.
|
| 60 |
const (
|
| 61 |
+
SubscriptionTypeStandard = domain.SubscriptionTypeStandard
|
| 62 |
+
SubscriptionTypeSubscription = domain.SubscriptionTypeSubscription
|
| 63 |
)
|
| 64 |
|
| 65 |
+
// Subscription status constants.
|
| 66 |
const (
|
| 67 |
SubscriptionStatusActive = domain.SubscriptionStatusActive
|
| 68 |
SubscriptionStatusExpired = domain.SubscriptionStatusExpired
|
| 69 |
SubscriptionStatusSuspended = domain.SubscriptionStatusSuspended
|
| 70 |
)
|
| 71 |
|
|
|
|
| 72 |
const LinuxDoConnectSyntheticEmailDomain = "@linuxdo-connect.invalid"
|
| 73 |
|
| 74 |
+
// Setting keys.
|
| 75 |
const (
|
| 76 |
+
SettingKeyRegistrationEnabled = "registration_enabled"
|
| 77 |
+
SettingKeyEmailVerifyEnabled = "email_verify_enabled"
|
| 78 |
+
SettingKeyRegistrationEmailSuffixWhitelist = "registration_email_suffix_whitelist"
|
| 79 |
+
SettingKeyPromoCodeEnabled = "promo_code_enabled"
|
| 80 |
+
SettingKeyPasswordResetEnabled = "password_reset_enabled"
|
| 81 |
+
SettingKeyFrontendURL = "frontend_url"
|
| 82 |
+
SettingKeyInvitationCodeEnabled = "invitation_code_enabled"
|
| 83 |
+
|
| 84 |
+
SettingKeySMTPHost = "smtp_host"
|
| 85 |
+
SettingKeySMTPPort = "smtp_port"
|
| 86 |
+
SettingKeySMTPUsername = "smtp_username"
|
| 87 |
+
SettingKeySMTPPassword = "smtp_password"
|
| 88 |
+
SettingKeySMTPFrom = "smtp_from"
|
| 89 |
+
SettingKeySMTPFromName = "smtp_from_name"
|
| 90 |
+
SettingKeySMTPUseTLS = "smtp_use_tls"
|
| 91 |
+
|
| 92 |
+
SettingKeyEmailProvider = "email_provider"
|
| 93 |
+
SettingKeyMailjetAPIKey = "mailjet_api_key"
|
| 94 |
+
SettingKeyMailjetSecretKey = "mailjet_secret_key"
|
| 95 |
+
SettingKeyMailjetFrom = "mailjet_from"
|
| 96 |
+
SettingKeyMailjetFromName = "mailjet_from_name"
|
| 97 |
+
|
| 98 |
+
SettingKeyTurnstileEnabled = "turnstile_enabled"
|
| 99 |
+
SettingKeyTurnstileSiteKey = "turnstile_site_key"
|
| 100 |
+
SettingKeyTurnstileSecretKey = "turnstile_secret_key"
|
| 101 |
+
|
| 102 |
+
SettingKeyTotpEnabled = "totp_enabled"
|
| 103 |
+
|
|
|
|
|
|
|
|
|
|
| 104 |
SettingKeyLinuxDoConnectEnabled = "linuxdo_connect_enabled"
|
| 105 |
SettingKeyLinuxDoConnectClientID = "linuxdo_connect_client_id"
|
| 106 |
SettingKeyLinuxDoConnectClientSecret = "linuxdo_connect_client_secret"
|
| 107 |
SettingKeyLinuxDoConnectRedirectURL = "linuxdo_connect_redirect_url"
|
| 108 |
|
| 109 |
+
SettingKeySoraClientEnabled = "sora_client_enabled"
|
| 110 |
+
SettingKeySiteName = "site_name"
|
| 111 |
+
SettingKeySiteLogo = "site_logo"
|
| 112 |
+
SettingKeySiteSubtitle = "site_subtitle"
|
| 113 |
+
SettingKeyAPIBaseURL = "api_base_url"
|
| 114 |
+
SettingKeyContactInfo = "contact_info"
|
| 115 |
+
SettingKeyDocURL = "doc_url"
|
| 116 |
+
SettingKeyHomeContent = "home_content"
|
| 117 |
+
SettingKeyHideCcsImportButton = "hide_ccs_import_button"
|
| 118 |
+
SettingKeyPurchaseSubscriptionEnabled = "purchase_subscription_enabled"
|
| 119 |
+
SettingKeyPurchaseSubscriptionURL = "purchase_subscription_url"
|
| 120 |
+
SettingKeyCustomMenuItems = "custom_menu_items"
|
| 121 |
+
|
| 122 |
+
SettingKeyDefaultConcurrency = "default_concurrency"
|
| 123 |
+
SettingKeyDefaultBalance = "default_balance"
|
| 124 |
+
SettingKeyDefaultSubscriptions = "default_subscriptions"
|
| 125 |
+
SettingKeyAdminAPIKey = "admin_api_key"
|
| 126 |
+
SettingKeyGeminiQuotaPolicy = "gemini_quota_policy"
|
| 127 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
SettingKeyEnableModelFallback = "enable_model_fallback"
|
| 129 |
SettingKeyFallbackModelAnthropic = "fallback_model_anthropic"
|
| 130 |
SettingKeyFallbackModelOpenAI = "fallback_model_openai"
|
| 131 |
SettingKeyFallbackModelGemini = "fallback_model_gemini"
|
| 132 |
SettingKeyFallbackModelAntigravity = "fallback_model_antigravity"
|
| 133 |
|
|
|
|
| 134 |
SettingKeyEnableIdentityPatch = "enable_identity_patch"
|
| 135 |
SettingKeyIdentityPatchPrompt = "identity_patch_prompt"
|
| 136 |
|
| 137 |
+
SettingKeyOpsMonitoringEnabled = "ops_monitoring_enabled"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
SettingKeyOpsRealtimeMonitoringEnabled = "ops_realtime_monitoring_enabled"
|
| 139 |
+
SettingKeyOpsQueryModeDefault = "ops_query_mode_default"
|
| 140 |
+
SettingKeyOpsEmailNotificationConfig = "ops_email_notification_config"
|
| 141 |
+
SettingKeyOpsAlertRuntimeSettings = "ops_alert_runtime_settings"
|
| 142 |
+
SettingKeyOpsMetricsIntervalSeconds = "ops_metrics_interval_seconds"
|
| 143 |
+
SettingKeyOpsAdvancedSettings = "ops_advanced_settings"
|
| 144 |
+
SettingKeyOpsRuntimeLogConfig = "ops_runtime_log_config"
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
SettingKeyOverloadCooldownSettings = "overload_cooldown_settings"
|
| 147 |
+
SettingKeyStreamTimeoutSettings = "stream_timeout_settings"
|
| 148 |
+
SettingKeyRectifierSettings = "rectifier_settings"
|
| 149 |
+
SettingKeyBetaPolicySettings = "beta_policy_settings"
|
| 150 |
+
|
| 151 |
+
SettingKeySoraS3Enabled = "sora_s3_enabled"
|
| 152 |
+
SettingKeySoraS3Endpoint = "sora_s3_endpoint"
|
| 153 |
+
SettingKeySoraS3Region = "sora_s3_region"
|
| 154 |
+
SettingKeySoraS3Bucket = "sora_s3_bucket"
|
| 155 |
+
SettingKeySoraS3AccessKeyID = "sora_s3_access_key_id"
|
| 156 |
+
SettingKeySoraS3SecretAccessKey = "sora_s3_secret_access_key"
|
| 157 |
+
SettingKeySoraS3Prefix = "sora_s3_prefix"
|
| 158 |
+
SettingKeySoraS3ForcePathStyle = "sora_s3_force_path_style"
|
| 159 |
+
SettingKeySoraS3CDNURL = "sora_s3_cdn_url"
|
| 160 |
+
SettingKeySoraS3Profiles = "sora_s3_profiles"
|
| 161 |
+
|
| 162 |
+
SettingKeySoraDefaultStorageQuotaBytes = "sora_default_storage_quota_bytes"
|
| 163 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
SettingKeyMinClaudeCodeVersion = "min_claude_code_version"
|
|
|
|
|
|
|
| 165 |
SettingKeyMaxClaudeCodeVersion = "max_claude_code_version"
|
| 166 |
|
|
|
|
| 167 |
SettingKeyAllowUngroupedKeyScheduling = "allow_ungrouped_key_scheduling"
|
| 168 |
+
SettingKeyBackendModeEnabled = "backend_mode_enabled"
|
|
|
|
|
|
|
| 169 |
)
|
| 170 |
|
| 171 |
+
// AdminAPIKeyPrefix is the prefix for admin API keys.
|
| 172 |
const AdminAPIKeyPrefix = "admin-"
|
backend/internal/service/email_service.go
CHANGED
|
@@ -74,10 +74,10 @@ const (
|
|
| 74 |
// Password reset email cooldown (prevent email bombing)
|
| 75 |
passwordResetEmailCooldown = 30 * time.Second
|
| 76 |
|
| 77 |
-
EmailProviderSMTP
|
| 78 |
-
|
| 79 |
|
| 80 |
-
|
| 81 |
)
|
| 82 |
|
| 83 |
// SMTPConfig SMTP配置
|
|
@@ -91,10 +91,11 @@ type SMTPConfig struct {
|
|
| 91 |
UseTLS bool
|
| 92 |
}
|
| 93 |
|
| 94 |
-
type
|
| 95 |
-
APIKey
|
| 96 |
-
|
| 97 |
-
|
|
|
|
| 98 |
}
|
| 99 |
|
| 100 |
// EmailService 邮件服务
|
|
@@ -102,7 +103,7 @@ type EmailService struct {
|
|
| 102 |
settingRepo SettingRepository
|
| 103 |
cache EmailCache
|
| 104 |
httpClient *http.Client
|
| 105 |
-
|
| 106 |
}
|
| 107 |
|
| 108 |
// NewEmailService 创建邮件服务实例
|
|
@@ -111,7 +112,7 @@ func NewEmailService(settingRepo SettingRepository, cache EmailCache) *EmailServ
|
|
| 111 |
settingRepo: settingRepo,
|
| 112 |
cache: cache,
|
| 113 |
httpClient: &http.Client{Timeout: 15 * time.Second},
|
| 114 |
-
|
| 115 |
}
|
| 116 |
}
|
| 117 |
|
|
@@ -157,24 +158,26 @@ func (s *EmailService) GetSMTPConfig(ctx context.Context) (*SMTPConfig, error) {
|
|
| 157 |
}, nil
|
| 158 |
}
|
| 159 |
|
| 160 |
-
func (s *EmailService)
|
| 161 |
keys := []string{
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
|
|
|
| 165 |
}
|
| 166 |
|
| 167 |
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
| 168 |
if err != nil {
|
| 169 |
-
return nil, fmt.Errorf("get
|
| 170 |
}
|
| 171 |
|
| 172 |
-
config := &
|
| 173 |
-
APIKey:
|
| 174 |
-
|
| 175 |
-
|
|
|
|
| 176 |
}
|
| 177 |
-
if config.APIKey == "" || config.From == "" {
|
| 178 |
return nil, ErrEmailNotConfigured
|
| 179 |
}
|
| 180 |
return config, nil
|
|
@@ -189,8 +192,8 @@ func (s *EmailService) GetEmailProvider(ctx context.Context) (string, error) {
|
|
| 189 |
return "", fmt.Errorf("get email provider: %w", err)
|
| 190 |
}
|
| 191 |
provider = strings.TrimSpace(provider)
|
| 192 |
-
if provider ==
|
| 193 |
-
return
|
| 194 |
}
|
| 195 |
return EmailProviderSMTP, nil
|
| 196 |
}
|
|
@@ -201,12 +204,12 @@ func (s *EmailService) SendEmail(ctx context.Context, to, subject, body string)
|
|
| 201 |
if err != nil {
|
| 202 |
return err
|
| 203 |
}
|
| 204 |
-
if provider ==
|
| 205 |
-
config, configErr := s.
|
| 206 |
if configErr != nil {
|
| 207 |
return configErr
|
| 208 |
}
|
| 209 |
-
return s.
|
| 210 |
}
|
| 211 |
|
| 212 |
config, configErr := s.GetSMTPConfig(ctx)
|
|
@@ -236,38 +239,61 @@ func (s *EmailService) SendEmailWithConfig(config *SMTPConfig, to, subject, body
|
|
| 236 |
return smtp.SendMail(addr, auth, config.From, []string{to}, []byte(msg))
|
| 237 |
}
|
| 238 |
|
| 239 |
-
func (s *EmailService)
|
| 240 |
-
if config == nil || strings.TrimSpace(config.APIKey) == "" || strings.TrimSpace(config.From) == "" {
|
| 241 |
return ErrEmailNotConfigured
|
| 242 |
}
|
| 243 |
|
| 244 |
-
|
| 245 |
-
"
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
}
|
| 250 |
|
| 251 |
bodyBytes, err := json.Marshal(payload)
|
| 252 |
if err != nil {
|
| 253 |
-
return fmt.Errorf("marshal
|
| 254 |
}
|
| 255 |
|
| 256 |
-
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.
|
| 257 |
if err != nil {
|
| 258 |
-
return fmt.Errorf("build
|
| 259 |
}
|
| 260 |
-
req.
|
| 261 |
req.Header.Set("Content-Type", "application/json")
|
| 262 |
|
| 263 |
resp, err := s.httpClient.Do(req)
|
| 264 |
if err != nil {
|
| 265 |
-
return fmt.Errorf("
|
| 266 |
}
|
| 267 |
defer func() { _ = resp.Body.Close() }()
|
| 268 |
|
| 269 |
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
| 270 |
-
return fmt.Errorf("
|
| 271 |
}
|
| 272 |
return nil
|
| 273 |
}
|
|
@@ -517,25 +543,25 @@ func (s *EmailService) TestSMTPConnectionWithConfig(config *SMTPConfig) error {
|
|
| 517 |
return client.Quit()
|
| 518 |
}
|
| 519 |
|
| 520 |
-
func (s *EmailService)
|
| 521 |
-
if config == nil || strings.TrimSpace(config.APIKey) == "" {
|
| 522 |
return ErrEmailNotConfigured
|
| 523 |
}
|
| 524 |
|
| 525 |
-
req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.
|
| 526 |
if err != nil {
|
| 527 |
-
return fmt.Errorf("build
|
| 528 |
}
|
| 529 |
-
req.
|
| 530 |
|
| 531 |
resp, err := s.httpClient.Do(req)
|
| 532 |
if err != nil {
|
| 533 |
-
return fmt.Errorf("
|
| 534 |
}
|
| 535 |
defer func() { _ = resp.Body.Close() }()
|
| 536 |
|
| 537 |
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
| 538 |
-
return fmt.Errorf("
|
| 539 |
}
|
| 540 |
return nil
|
| 541 |
}
|
|
|
|
| 74 |
// Password reset email cooldown (prevent email bombing)
|
| 75 |
passwordResetEmailCooldown = 30 * time.Second
|
| 76 |
|
| 77 |
+
EmailProviderSMTP = "smtp"
|
| 78 |
+
EmailProviderMailjet = "mailjet"
|
| 79 |
|
| 80 |
+
defaultMailjetAPIBaseURL = "https://api.mailjet.com"
|
| 81 |
)
|
| 82 |
|
| 83 |
// SMTPConfig SMTP配置
|
|
|
|
| 91 |
UseTLS bool
|
| 92 |
}
|
| 93 |
|
| 94 |
+
type MailjetConfig struct {
|
| 95 |
+
APIKey string
|
| 96 |
+
SecretKey string
|
| 97 |
+
From string
|
| 98 |
+
FromName string
|
| 99 |
}
|
| 100 |
|
| 101 |
// EmailService 邮件服务
|
|
|
|
| 103 |
settingRepo SettingRepository
|
| 104 |
cache EmailCache
|
| 105 |
httpClient *http.Client
|
| 106 |
+
mailjetAPIBaseURL string
|
| 107 |
}
|
| 108 |
|
| 109 |
// NewEmailService 创建邮件服务实例
|
|
|
|
| 112 |
settingRepo: settingRepo,
|
| 113 |
cache: cache,
|
| 114 |
httpClient: &http.Client{Timeout: 15 * time.Second},
|
| 115 |
+
mailjetAPIBaseURL: defaultMailjetAPIBaseURL,
|
| 116 |
}
|
| 117 |
}
|
| 118 |
|
|
|
|
| 158 |
}, nil
|
| 159 |
}
|
| 160 |
|
| 161 |
+
func (s *EmailService) GetMailjetConfig(ctx context.Context) (*MailjetConfig, error) {
|
| 162 |
keys := []string{
|
| 163 |
+
SettingKeyMailjetAPIKey,
|
| 164 |
+
SettingKeyMailjetSecretKey,
|
| 165 |
+
SettingKeyMailjetFrom,
|
| 166 |
+
SettingKeyMailjetFromName,
|
| 167 |
}
|
| 168 |
|
| 169 |
settings, err := s.settingRepo.GetMultiple(ctx, keys)
|
| 170 |
if err != nil {
|
| 171 |
+
return nil, fmt.Errorf("get mailjet settings: %w", err)
|
| 172 |
}
|
| 173 |
|
| 174 |
+
config := &MailjetConfig{
|
| 175 |
+
APIKey: strings.TrimSpace(settings[SettingKeyMailjetAPIKey]),
|
| 176 |
+
SecretKey: strings.TrimSpace(settings[SettingKeyMailjetSecretKey]),
|
| 177 |
+
From: strings.TrimSpace(settings[SettingKeyMailjetFrom]),
|
| 178 |
+
FromName: strings.TrimSpace(settings[SettingKeyMailjetFromName]),
|
| 179 |
}
|
| 180 |
+
if config.APIKey == "" || config.SecretKey == "" || config.From == "" {
|
| 181 |
return nil, ErrEmailNotConfigured
|
| 182 |
}
|
| 183 |
return config, nil
|
|
|
|
| 192 |
return "", fmt.Errorf("get email provider: %w", err)
|
| 193 |
}
|
| 194 |
provider = strings.TrimSpace(provider)
|
| 195 |
+
if provider == EmailProviderMailjet {
|
| 196 |
+
return EmailProviderMailjet, nil
|
| 197 |
}
|
| 198 |
return EmailProviderSMTP, nil
|
| 199 |
}
|
|
|
|
| 204 |
if err != nil {
|
| 205 |
return err
|
| 206 |
}
|
| 207 |
+
if provider == EmailProviderMailjet {
|
| 208 |
+
config, configErr := s.GetMailjetConfig(ctx)
|
| 209 |
if configErr != nil {
|
| 210 |
return configErr
|
| 211 |
}
|
| 212 |
+
return s.SendEmailWithMailjetConfig(ctx, config, to, subject, body)
|
| 213 |
}
|
| 214 |
|
| 215 |
config, configErr := s.GetSMTPConfig(ctx)
|
|
|
|
| 239 |
return smtp.SendMail(addr, auth, config.From, []string{to}, []byte(msg))
|
| 240 |
}
|
| 241 |
|
| 242 |
+
func (s *EmailService) SendEmailWithMailjetConfig(ctx context.Context, config *MailjetConfig, to, subject, body string) error {
|
| 243 |
+
if config == nil || strings.TrimSpace(config.APIKey) == "" || strings.TrimSpace(config.SecretKey) == "" || strings.TrimSpace(config.From) == "" {
|
| 244 |
return ErrEmailNotConfigured
|
| 245 |
}
|
| 246 |
|
| 247 |
+
type mailjetContact struct {
|
| 248 |
+
Email string `json:"Email"`
|
| 249 |
+
Name string `json:"Name,omitempty"`
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
type mailjetMessage struct {
|
| 253 |
+
From mailjetContact `json:"From"`
|
| 254 |
+
To []mailjetContact `json:"To"`
|
| 255 |
+
Subject string `json:"Subject"`
|
| 256 |
+
HTMLPart string `json:"HTMLPart"`
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
payload := struct {
|
| 260 |
+
Messages []mailjetMessage `json:"Messages"`
|
| 261 |
+
}{
|
| 262 |
+
Messages: []mailjetMessage{
|
| 263 |
+
{
|
| 264 |
+
From: mailjetContact{
|
| 265 |
+
Email: config.From,
|
| 266 |
+
Name: strings.TrimSpace(config.FromName),
|
| 267 |
+
},
|
| 268 |
+
To: []mailjetContact{
|
| 269 |
+
{Email: to},
|
| 270 |
+
},
|
| 271 |
+
Subject: subject,
|
| 272 |
+
HTMLPart: body,
|
| 273 |
+
},
|
| 274 |
+
},
|
| 275 |
}
|
| 276 |
|
| 277 |
bodyBytes, err := json.Marshal(payload)
|
| 278 |
if err != nil {
|
| 279 |
+
return fmt.Errorf("marshal mailjet payload: %w", err)
|
| 280 |
}
|
| 281 |
|
| 282 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.mailjetAPIBaseURL+"/v3.1/send", bytes.NewReader(bodyBytes))
|
| 283 |
if err != nil {
|
| 284 |
+
return fmt.Errorf("build mailjet request: %w", err)
|
| 285 |
}
|
| 286 |
+
req.SetBasicAuth(config.APIKey, config.SecretKey)
|
| 287 |
req.Header.Set("Content-Type", "application/json")
|
| 288 |
|
| 289 |
resp, err := s.httpClient.Do(req)
|
| 290 |
if err != nil {
|
| 291 |
+
return fmt.Errorf("mailjet request failed: %w", err)
|
| 292 |
}
|
| 293 |
defer func() { _ = resp.Body.Close() }()
|
| 294 |
|
| 295 |
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
| 296 |
+
return fmt.Errorf("mailjet send failed: %s", readEmailAPIError(resp))
|
| 297 |
}
|
| 298 |
return nil
|
| 299 |
}
|
|
|
|
| 543 |
return client.Quit()
|
| 544 |
}
|
| 545 |
|
| 546 |
+
func (s *EmailService) TestMailjetConnectionWithConfig(ctx context.Context, config *MailjetConfig) error {
|
| 547 |
+
if config == nil || strings.TrimSpace(config.APIKey) == "" || strings.TrimSpace(config.SecretKey) == "" {
|
| 548 |
return ErrEmailNotConfigured
|
| 549 |
}
|
| 550 |
|
| 551 |
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, s.mailjetAPIBaseURL+"/v3/REST/sender", nil)
|
| 552 |
if err != nil {
|
| 553 |
+
return fmt.Errorf("build mailjet request: %w", err)
|
| 554 |
}
|
| 555 |
+
req.SetBasicAuth(config.APIKey, config.SecretKey)
|
| 556 |
|
| 557 |
resp, err := s.httpClient.Do(req)
|
| 558 |
if err != nil {
|
| 559 |
+
return fmt.Errorf("mailjet request failed: %w", err)
|
| 560 |
}
|
| 561 |
defer func() { _ = resp.Body.Close() }()
|
| 562 |
|
| 563 |
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
| 564 |
+
return fmt.Errorf("mailjet connection failed: %s", readEmailAPIError(resp))
|
| 565 |
}
|
| 566 |
return nil
|
| 567 |
}
|
backend/internal/service/email_service_resend_test.go
CHANGED
|
@@ -4,14 +4,23 @@ package service
|
|
| 4 |
|
| 5 |
import (
|
| 6 |
"context"
|
|
|
|
| 7 |
"encoding/json"
|
|
|
|
| 8 |
"net/http"
|
| 9 |
"net/http/httptest"
|
|
|
|
| 10 |
"testing"
|
| 11 |
|
| 12 |
"github.com/stretchr/testify/require"
|
| 13 |
)
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
type emailServiceRepoStub struct {
|
| 16 |
settings map[string]string
|
| 17 |
}
|
|
@@ -53,56 +62,75 @@ func (s *emailServiceRepoStub) Delete(ctx context.Context, key string) error {
|
|
| 53 |
panic("unexpected Delete call")
|
| 54 |
}
|
| 55 |
|
| 56 |
-
func
|
| 57 |
-
type
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
}
|
| 63 |
|
| 64 |
var (
|
| 65 |
gotMethod string
|
| 66 |
gotPath string
|
| 67 |
gotAuth string
|
| 68 |
-
gotPayload
|
| 69 |
)
|
| 70 |
|
| 71 |
-
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 72 |
-
gotMethod = r.Method
|
| 73 |
-
gotPath = r.URL.Path
|
| 74 |
-
gotAuth = r.Header.Get("Authorization")
|
| 75 |
-
|
| 76 |
-
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotPayload))
|
| 77 |
-
w.WriteHeader(http.StatusAccepted)
|
| 78 |
-
}))
|
| 79 |
-
defer server.Close()
|
| 80 |
-
|
| 81 |
repo := &emailServiceRepoStub{
|
| 82 |
settings: map[string]string{
|
| 83 |
-
SettingKeyEmailProvider:
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
|
|
|
| 87 |
},
|
| 88 |
}
|
| 89 |
|
| 90 |
svc := NewEmailService(repo, nil)
|
| 91 |
-
svc.httpClient =
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
|
| 94 |
err := svc.SendEmail(context.Background(), "user@example.com", "Hello", "<p>World</p>")
|
| 95 |
require.NoError(t, err)
|
| 96 |
require.Equal(t, http.MethodPost, gotMethod)
|
| 97 |
-
require.Equal(t, "/
|
| 98 |
-
require.Equal(t, "
|
| 99 |
-
require.
|
| 100 |
-
require.Equal(t,
|
| 101 |
-
require.Equal(t, "
|
| 102 |
-
require.Equal(t, "
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
|
| 105 |
-
func
|
| 106 |
var (
|
| 107 |
gotMethod string
|
| 108 |
gotPath string
|
|
@@ -119,18 +147,19 @@ func TestEmailService_TestResendConnectionWithConfig(t *testing.T) {
|
|
| 119 |
|
| 120 |
svc := NewEmailService(&emailServiceRepoStub{}, nil)
|
| 121 |
svc.httpClient = server.Client()
|
| 122 |
-
svc.
|
| 123 |
|
| 124 |
-
err := svc.
|
| 125 |
-
APIKey:
|
|
|
|
| 126 |
})
|
| 127 |
require.NoError(t, err)
|
| 128 |
require.Equal(t, http.MethodGet, gotMethod)
|
| 129 |
-
require.Equal(t, "/
|
| 130 |
-
require.Equal(t, "
|
| 131 |
}
|
| 132 |
|
| 133 |
-
func
|
| 134 |
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 135 |
w.WriteHeader(http.StatusBadRequest)
|
| 136 |
_, _ = w.Write([]byte(`{"message":"invalid from address"}`))
|
|
@@ -139,12 +168,13 @@ func TestEmailService_SendEmailWithResendConfig_ReportsAPIError(t *testing.T) {
|
|
| 139 |
|
| 140 |
svc := NewEmailService(&emailServiceRepoStub{}, nil)
|
| 141 |
svc.httpClient = server.Client()
|
| 142 |
-
svc.
|
| 143 |
|
| 144 |
-
err := svc.
|
| 145 |
-
APIKey:
|
| 146 |
-
|
| 147 |
-
|
|
|
|
| 148 |
}, "user@example.com", "Hello", "<p>World</p>")
|
| 149 |
|
| 150 |
require.Error(t, err)
|
|
|
|
| 4 |
|
| 5 |
import (
|
| 6 |
"context"
|
| 7 |
+
"encoding/base64"
|
| 8 |
"encoding/json"
|
| 9 |
+
"io"
|
| 10 |
"net/http"
|
| 11 |
"net/http/httptest"
|
| 12 |
+
"strings"
|
| 13 |
"testing"
|
| 14 |
|
| 15 |
"github.com/stretchr/testify/require"
|
| 16 |
)
|
| 17 |
|
| 18 |
+
type roundTripFunc func(*http.Request) (*http.Response, error)
|
| 19 |
+
|
| 20 |
+
func (fn roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
|
| 21 |
+
return fn(req)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
type emailServiceRepoStub struct {
|
| 25 |
settings map[string]string
|
| 26 |
}
|
|
|
|
| 62 |
panic("unexpected Delete call")
|
| 63 |
}
|
| 64 |
|
| 65 |
+
func TestEmailService_SendEmail_UsesMailjetProvider(t *testing.T) {
|
| 66 |
+
type mailjetRecipient struct {
|
| 67 |
+
Email string `json:"Email"`
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
type mailjetSender struct {
|
| 71 |
+
Email string `json:"Email"`
|
| 72 |
+
Name string `json:"Name"`
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
type mailjetMessage struct {
|
| 76 |
+
From mailjetSender `json:"From"`
|
| 77 |
+
To []mailjetRecipient `json:"To"`
|
| 78 |
+
Subject string `json:"Subject"`
|
| 79 |
+
HTMLPart string `json:"HTMLPart"`
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
type mailjetPayload struct {
|
| 83 |
+
Messages []mailjetMessage `json:"Messages"`
|
| 84 |
}
|
| 85 |
|
| 86 |
var (
|
| 87 |
gotMethod string
|
| 88 |
gotPath string
|
| 89 |
gotAuth string
|
| 90 |
+
gotPayload mailjetPayload
|
| 91 |
)
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
repo := &emailServiceRepoStub{
|
| 94 |
settings: map[string]string{
|
| 95 |
+
SettingKeyEmailProvider: EmailProviderMailjet,
|
| 96 |
+
SettingKeyMailjetAPIKey: "mj_api_key",
|
| 97 |
+
SettingKeyMailjetSecretKey: "mj_secret_key",
|
| 98 |
+
SettingKeyMailjetFrom: "noreply@example.com",
|
| 99 |
+
SettingKeyMailjetFromName: "XAPI",
|
| 100 |
},
|
| 101 |
}
|
| 102 |
|
| 103 |
svc := NewEmailService(repo, nil)
|
| 104 |
+
svc.httpClient = &http.Client{
|
| 105 |
+
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
| 106 |
+
gotMethod = r.Method
|
| 107 |
+
gotPath = r.URL.Path
|
| 108 |
+
gotAuth = r.Header.Get("Authorization")
|
| 109 |
+
require.NoError(t, json.NewDecoder(r.Body).Decode(&gotPayload))
|
| 110 |
+
return &http.Response{
|
| 111 |
+
StatusCode: http.StatusOK,
|
| 112 |
+
Status: "200 OK",
|
| 113 |
+
Header: make(http.Header),
|
| 114 |
+
Body: io.NopCloser(strings.NewReader(`{"Messages":[{"Status":"success"}]}`)),
|
| 115 |
+
Request: r,
|
| 116 |
+
}, nil
|
| 117 |
+
}),
|
| 118 |
+
}
|
| 119 |
|
| 120 |
err := svc.SendEmail(context.Background(), "user@example.com", "Hello", "<p>World</p>")
|
| 121 |
require.NoError(t, err)
|
| 122 |
require.Equal(t, http.MethodPost, gotMethod)
|
| 123 |
+
require.Equal(t, "/v3.1/send", gotPath)
|
| 124 |
+
require.Equal(t, "Basic "+base64.StdEncoding.EncodeToString([]byte("mj_api_key:mj_secret_key")), gotAuth)
|
| 125 |
+
require.Len(t, gotPayload.Messages, 1)
|
| 126 |
+
require.Equal(t, "noreply@example.com", gotPayload.Messages[0].From.Email)
|
| 127 |
+
require.Equal(t, "XAPI", gotPayload.Messages[0].From.Name)
|
| 128 |
+
require.Equal(t, []mailjetRecipient{{Email: "user@example.com"}}, gotPayload.Messages[0].To)
|
| 129 |
+
require.Equal(t, "Hello", gotPayload.Messages[0].Subject)
|
| 130 |
+
require.Equal(t, "<p>World</p>", gotPayload.Messages[0].HTMLPart)
|
| 131 |
}
|
| 132 |
|
| 133 |
+
func TestEmailService_TestMailjetConnectionWithConfig(t *testing.T) {
|
| 134 |
var (
|
| 135 |
gotMethod string
|
| 136 |
gotPath string
|
|
|
|
| 147 |
|
| 148 |
svc := NewEmailService(&emailServiceRepoStub{}, nil)
|
| 149 |
svc.httpClient = server.Client()
|
| 150 |
+
svc.mailjetAPIBaseURL = server.URL
|
| 151 |
|
| 152 |
+
err := svc.TestMailjetConnectionWithConfig(context.Background(), &MailjetConfig{
|
| 153 |
+
APIKey: "mj_api_key",
|
| 154 |
+
SecretKey: "mj_secret_key",
|
| 155 |
})
|
| 156 |
require.NoError(t, err)
|
| 157 |
require.Equal(t, http.MethodGet, gotMethod)
|
| 158 |
+
require.Equal(t, "/v3/REST/sender", gotPath)
|
| 159 |
+
require.Equal(t, "Basic "+base64.StdEncoding.EncodeToString([]byte("mj_api_key:mj_secret_key")), gotAuth)
|
| 160 |
}
|
| 161 |
|
| 162 |
+
func TestEmailService_SendEmailWithMailjetConfig_ReportsAPIError(t *testing.T) {
|
| 163 |
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
| 164 |
w.WriteHeader(http.StatusBadRequest)
|
| 165 |
_, _ = w.Write([]byte(`{"message":"invalid from address"}`))
|
|
|
|
| 168 |
|
| 169 |
svc := NewEmailService(&emailServiceRepoStub{}, nil)
|
| 170 |
svc.httpClient = server.Client()
|
| 171 |
+
svc.mailjetAPIBaseURL = server.URL
|
| 172 |
|
| 173 |
+
err := svc.SendEmailWithMailjetConfig(context.Background(), &MailjetConfig{
|
| 174 |
+
APIKey: "mj_api_key",
|
| 175 |
+
SecretKey: "mj_secret_key",
|
| 176 |
+
From: "noreply@example.com",
|
| 177 |
+
FromName: "XAPI",
|
| 178 |
}, "user@example.com", "Hello", "<p>World</p>")
|
| 179 |
|
| 180 |
require.Error(t, err)
|
backend/internal/service/setting_service.go
CHANGED
|
@@ -429,11 +429,14 @@ func (s *SettingService) UpdateSettings(ctx context.Context, settings *SystemSet
|
|
| 429 |
updates[SettingKeySMTPFromName] = settings.SMTPFromName
|
| 430 |
updates[SettingKeySMTPUseTLS] = strconv.FormatBool(settings.SMTPUseTLS)
|
| 431 |
updates[SettingKeyEmailProvider] = settings.EmailProvider
|
| 432 |
-
if settings.
|
| 433 |
-
updates[
|
| 434 |
}
|
| 435 |
-
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
| 437 |
|
| 438 |
// Cloudflare Turnstile 设置(只有非空才更新密钥)
|
| 439 |
updates[SettingKeyTurnstileEnabled] = strconv.FormatBool(settings.TurnstileEnabled)
|
|
@@ -755,9 +758,10 @@ func (s *SettingService) InitializeDefaultSettings(ctx context.Context) error {
|
|
| 755 |
SettingKeySMTPPort: "587",
|
| 756 |
SettingKeySMTPUseTLS: "false",
|
| 757 |
SettingKeyEmailProvider: EmailProviderSMTP,
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
|
|
|
| 761 |
// Model fallback defaults
|
| 762 |
SettingKeyEnableModelFallback: "false",
|
| 763 |
SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022",
|
|
@@ -804,9 +808,10 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|
| 804 |
SMTPUseTLS: settings[SettingKeySMTPUseTLS] == "true",
|
| 805 |
SMTPPasswordConfigured: settings[SettingKeySMTPPassword] != "",
|
| 806 |
EmailProvider: s.getStringOrDefault(settings, SettingKeyEmailProvider, EmailProviderSMTP),
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
|
|
|
|
| 810 |
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
| 811 |
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
| 812 |
TurnstileSecretKeyConfigured: settings[SettingKeyTurnstileSecretKey] != "",
|
|
@@ -848,9 +853,10 @@ func (s *SettingService) parseSettings(settings map[string]string) *SystemSettin
|
|
| 848 |
|
| 849 |
// 敏感信息直接返回,方便测试连接时使用
|
| 850 |
result.SMTPPassword = settings[SettingKeySMTPPassword]
|
| 851 |
-
result.
|
|
|
|
| 852 |
result.TurnstileSecretKey = settings[SettingKeyTurnstileSecretKey]
|
| 853 |
-
if result.EmailProvider != EmailProviderSMTP && result.EmailProvider !=
|
| 854 |
result.EmailProvider = EmailProviderSMTP
|
| 855 |
}
|
| 856 |
|
|
|
|
| 429 |
updates[SettingKeySMTPFromName] = settings.SMTPFromName
|
| 430 |
updates[SettingKeySMTPUseTLS] = strconv.FormatBool(settings.SMTPUseTLS)
|
| 431 |
updates[SettingKeyEmailProvider] = settings.EmailProvider
|
| 432 |
+
if settings.MailjetAPIKey != "" {
|
| 433 |
+
updates[SettingKeyMailjetAPIKey] = settings.MailjetAPIKey
|
| 434 |
}
|
| 435 |
+
if settings.MailjetSecretKey != "" {
|
| 436 |
+
updates[SettingKeyMailjetSecretKey] = settings.MailjetSecretKey
|
| 437 |
+
}
|
| 438 |
+
updates[SettingKeyMailjetFrom] = settings.MailjetFrom
|
| 439 |
+
updates[SettingKeyMailjetFromName] = settings.MailjetFromName
|
| 440 |
|
| 441 |
// Cloudflare Turnstile 设置(只有非空才更新密钥)
|
| 442 |
updates[SettingKeyTurnstileEnabled] = strconv.FormatBool(settings.TurnstileEnabled)
|
|
|
|
| 758 |
SettingKeySMTPPort: "587",
|
| 759 |
SettingKeySMTPUseTLS: "false",
|
| 760 |
SettingKeyEmailProvider: EmailProviderSMTP,
|
| 761 |
+
SettingKeyMailjetAPIKey: "",
|
| 762 |
+
SettingKeyMailjetSecretKey: "",
|
| 763 |
+
SettingKeyMailjetFrom: "",
|
| 764 |
+
SettingKeyMailjetFromName: "",
|
| 765 |
// Model fallback defaults
|
| 766 |
SettingKeyEnableModelFallback: "false",
|
| 767 |
SettingKeyFallbackModelAnthropic: "claude-3-5-sonnet-20241022",
|
|
|
|
| 808 |
SMTPUseTLS: settings[SettingKeySMTPUseTLS] == "true",
|
| 809 |
SMTPPasswordConfigured: settings[SettingKeySMTPPassword] != "",
|
| 810 |
EmailProvider: s.getStringOrDefault(settings, SettingKeyEmailProvider, EmailProviderSMTP),
|
| 811 |
+
MailjetFrom: settings[SettingKeyMailjetFrom],
|
| 812 |
+
MailjetFromName: settings[SettingKeyMailjetFromName],
|
| 813 |
+
MailjetAPIKeyConfigured: settings[SettingKeyMailjetAPIKey] != "",
|
| 814 |
+
MailjetSecretKeyConfigured: settings[SettingKeyMailjetSecretKey] != "",
|
| 815 |
TurnstileEnabled: settings[SettingKeyTurnstileEnabled] == "true",
|
| 816 |
TurnstileSiteKey: settings[SettingKeyTurnstileSiteKey],
|
| 817 |
TurnstileSecretKeyConfigured: settings[SettingKeyTurnstileSecretKey] != "",
|
|
|
|
| 853 |
|
| 854 |
// 敏感信息直接返回,方便测试连接时使用
|
| 855 |
result.SMTPPassword = settings[SettingKeySMTPPassword]
|
| 856 |
+
result.MailjetAPIKey = settings[SettingKeyMailjetAPIKey]
|
| 857 |
+
result.MailjetSecretKey = settings[SettingKeyMailjetSecretKey]
|
| 858 |
result.TurnstileSecretKey = settings[SettingKeyTurnstileSecretKey]
|
| 859 |
+
if result.EmailProvider != EmailProviderSMTP && result.EmailProvider != EmailProviderMailjet {
|
| 860 |
result.EmailProvider = EmailProviderSMTP
|
| 861 |
}
|
| 862 |
|
backend/internal/service/setting_service_update_test.go
CHANGED
|
@@ -194,39 +194,44 @@ func TestSettingService_UpdateSettings_RegistrationEmailSuffixWhitelist_Invalid(
|
|
| 194 |
require.Equal(t, "INVALID_REGISTRATION_EMAIL_SUFFIX_WHITELIST", infraerrors.Reason(err))
|
| 195 |
}
|
| 196 |
|
| 197 |
-
func
|
| 198 |
repo := &settingUpdateRepoStub{}
|
| 199 |
svc := NewSettingService(repo, &config.Config{})
|
| 200 |
|
| 201 |
err := svc.UpdateSettings(context.Background(), &SystemSettings{
|
| 202 |
-
EmailProvider:
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
| 206 |
})
|
| 207 |
|
| 208 |
require.NoError(t, err)
|
| 209 |
-
require.Equal(t,
|
| 210 |
-
require.Equal(t, "
|
| 211 |
-
require.Equal(t, "
|
| 212 |
-
require.Equal(t, "
|
|
|
|
| 213 |
}
|
| 214 |
|
| 215 |
-
func
|
| 216 |
svc := NewSettingService(&settingUpdateRepoStub{}, &config.Config{})
|
| 217 |
|
| 218 |
settings := svc.parseSettings(map[string]string{
|
| 219 |
-
SettingKeyEmailProvider:
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
|
|
|
| 223 |
})
|
| 224 |
|
| 225 |
-
require.Equal(t,
|
| 226 |
-
require.True(t, settings.
|
| 227 |
-
require.
|
| 228 |
-
require.Equal(t, "
|
| 229 |
-
require.Equal(t, "
|
|
|
|
|
|
|
| 230 |
}
|
| 231 |
|
| 232 |
func TestParseDefaultSubscriptions_NormalizesValues(t *testing.T) {
|
|
|
|
| 194 |
require.Equal(t, "INVALID_REGISTRATION_EMAIL_SUFFIX_WHITELIST", infraerrors.Reason(err))
|
| 195 |
}
|
| 196 |
|
| 197 |
+
func TestSettingService_UpdateSettings_PersistsMailjetSettings(t *testing.T) {
|
| 198 |
repo := &settingUpdateRepoStub{}
|
| 199 |
svc := NewSettingService(repo, &config.Config{})
|
| 200 |
|
| 201 |
err := svc.UpdateSettings(context.Background(), &SystemSettings{
|
| 202 |
+
EmailProvider: EmailProviderMailjet,
|
| 203 |
+
MailjetAPIKey: "mj_api_key",
|
| 204 |
+
MailjetSecretKey: "mj_secret_key",
|
| 205 |
+
MailjetFrom: "noreply@example.com",
|
| 206 |
+
MailjetFromName: "XAPI",
|
| 207 |
})
|
| 208 |
|
| 209 |
require.NoError(t, err)
|
| 210 |
+
require.Equal(t, EmailProviderMailjet, repo.updates[SettingKeyEmailProvider])
|
| 211 |
+
require.Equal(t, "mj_api_key", repo.updates[SettingKeyMailjetAPIKey])
|
| 212 |
+
require.Equal(t, "mj_secret_key", repo.updates[SettingKeyMailjetSecretKey])
|
| 213 |
+
require.Equal(t, "noreply@example.com", repo.updates[SettingKeyMailjetFrom])
|
| 214 |
+
require.Equal(t, "XAPI", repo.updates[SettingKeyMailjetFromName])
|
| 215 |
}
|
| 216 |
|
| 217 |
+
func TestSettingService_ParseSettings_ExposesMailjetState(t *testing.T) {
|
| 218 |
svc := NewSettingService(&settingUpdateRepoStub{}, &config.Config{})
|
| 219 |
|
| 220 |
settings := svc.parseSettings(map[string]string{
|
| 221 |
+
SettingKeyEmailProvider: EmailProviderMailjet,
|
| 222 |
+
SettingKeyMailjetAPIKey: "mj_api_key",
|
| 223 |
+
SettingKeyMailjetSecretKey: "mj_secret_key",
|
| 224 |
+
SettingKeyMailjetFrom: "noreply@example.com",
|
| 225 |
+
SettingKeyMailjetFromName: "XAPI",
|
| 226 |
})
|
| 227 |
|
| 228 |
+
require.Equal(t, EmailProviderMailjet, settings.EmailProvider)
|
| 229 |
+
require.True(t, settings.MailjetAPIKeyConfigured)
|
| 230 |
+
require.True(t, settings.MailjetSecretKeyConfigured)
|
| 231 |
+
require.Equal(t, "mj_api_key", settings.MailjetAPIKey)
|
| 232 |
+
require.Equal(t, "mj_secret_key", settings.MailjetSecretKey)
|
| 233 |
+
require.Equal(t, "noreply@example.com", settings.MailjetFrom)
|
| 234 |
+
require.Equal(t, "XAPI", settings.MailjetFromName)
|
| 235 |
}
|
| 236 |
|
| 237 |
func TestParseDefaultSubscriptions_NormalizesValues(t *testing.T) {
|
backend/internal/service/settings_view.go
CHANGED
|
@@ -18,11 +18,13 @@ type SystemSettings struct {
|
|
| 18 |
SMTPFrom string
|
| 19 |
SMTPFromName string
|
| 20 |
SMTPUseTLS bool
|
| 21 |
-
EmailProvider
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
| 26 |
|
| 27 |
TurnstileEnabled bool
|
| 28 |
TurnstileSiteKey string
|
|
|
|
| 18 |
SMTPFrom string
|
| 19 |
SMTPFromName string
|
| 20 |
SMTPUseTLS bool
|
| 21 |
+
EmailProvider string
|
| 22 |
+
MailjetAPIKey string
|
| 23 |
+
MailjetSecretKey string
|
| 24 |
+
MailjetAPIKeyConfigured bool
|
| 25 |
+
MailjetSecretKeyConfigured bool
|
| 26 |
+
MailjetFrom string
|
| 27 |
+
MailjetFromName string
|
| 28 |
|
| 29 |
TurnstileEnabled bool
|
| 30 |
TurnstileSiteKey string
|
frontend/src/api/admin/settings.ts
CHANGED
|
@@ -51,10 +51,11 @@ export interface SystemSettings {
|
|
| 51 |
smtp_from_email: string
|
| 52 |
smtp_from_name: string
|
| 53 |
smtp_use_tls: boolean
|
| 54 |
-
email_provider: 'smtp' | '
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
|
|
|
| 58 |
// Cloudflare Turnstile settings
|
| 59 |
turnstile_enabled: boolean
|
| 60 |
turnstile_site_key: string
|
|
@@ -123,10 +124,11 @@ export interface UpdateSettingsRequest {
|
|
| 123 |
smtp_from_email?: string
|
| 124 |
smtp_from_name?: string
|
| 125 |
smtp_use_tls?: boolean
|
| 126 |
-
email_provider?: 'smtp' | '
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
|
|
|
| 130 |
turnstile_enabled?: boolean
|
| 131 |
turnstile_site_key?: string
|
| 132 |
turnstile_secret_key?: string
|
|
@@ -180,8 +182,9 @@ export interface TestSmtpRequest {
|
|
| 180 |
smtp_use_tls: boolean
|
| 181 |
}
|
| 182 |
|
| 183 |
-
export interface
|
| 184 |
-
|
|
|
|
| 185 |
}
|
| 186 |
|
| 187 |
/**
|
|
@@ -194,8 +197,8 @@ export async function testSmtpConnection(config: TestSmtpRequest): Promise<{ mes
|
|
| 194 |
return data
|
| 195 |
}
|
| 196 |
|
| 197 |
-
export async function
|
| 198 |
-
const { data } = await apiClient.post<{ message: string }>('/admin/settings/test-
|
| 199 |
return data
|
| 200 |
}
|
| 201 |
|
|
@@ -213,11 +216,12 @@ export interface SendTestEmailRequest {
|
|
| 213 |
smtp_use_tls: boolean
|
| 214 |
}
|
| 215 |
|
| 216 |
-
export interface
|
| 217 |
email: string
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
|
|
|
| 221 |
}
|
| 222 |
|
| 223 |
/**
|
|
@@ -233,11 +237,11 @@ export async function sendTestEmail(request: SendTestEmailRequest): Promise<{ me
|
|
| 233 |
return data
|
| 234 |
}
|
| 235 |
|
| 236 |
-
export async function
|
| 237 |
-
request:
|
| 238 |
): Promise<{ message: string }> {
|
| 239 |
const { data } = await apiClient.post<{ message: string }>(
|
| 240 |
-
'/admin/settings/send-test-email-
|
| 241 |
request
|
| 242 |
)
|
| 243 |
return data
|
|
@@ -558,9 +562,9 @@ export const settingsAPI = {
|
|
| 558 |
getSettings,
|
| 559 |
updateSettings,
|
| 560 |
testSmtpConnection,
|
| 561 |
-
|
| 562 |
sendTestEmail,
|
| 563 |
-
|
| 564 |
getAdminApiKey,
|
| 565 |
regenerateAdminApiKey,
|
| 566 |
deleteAdminApiKey,
|
|
|
|
| 51 |
smtp_from_email: string
|
| 52 |
smtp_from_name: string
|
| 53 |
smtp_use_tls: boolean
|
| 54 |
+
email_provider: 'smtp' | 'mailjet' | string
|
| 55 |
+
mailjet_api_key_configured: boolean
|
| 56 |
+
mailjet_secret_key_configured: boolean
|
| 57 |
+
mailjet_from_email: string
|
| 58 |
+
mailjet_from_name: string
|
| 59 |
// Cloudflare Turnstile settings
|
| 60 |
turnstile_enabled: boolean
|
| 61 |
turnstile_site_key: string
|
|
|
|
| 124 |
smtp_from_email?: string
|
| 125 |
smtp_from_name?: string
|
| 126 |
smtp_use_tls?: boolean
|
| 127 |
+
email_provider?: 'smtp' | 'mailjet' | string
|
| 128 |
+
mailjet_api_key?: string
|
| 129 |
+
mailjet_secret_key?: string
|
| 130 |
+
mailjet_from_email?: string
|
| 131 |
+
mailjet_from_name?: string
|
| 132 |
turnstile_enabled?: boolean
|
| 133 |
turnstile_site_key?: string
|
| 134 |
turnstile_secret_key?: string
|
|
|
|
| 182 |
smtp_use_tls: boolean
|
| 183 |
}
|
| 184 |
|
| 185 |
+
export interface TestMailjetRequest {
|
| 186 |
+
mailjet_api_key: string
|
| 187 |
+
mailjet_secret_key: string
|
| 188 |
}
|
| 189 |
|
| 190 |
/**
|
|
|
|
| 197 |
return data
|
| 198 |
}
|
| 199 |
|
| 200 |
+
export async function testMailjetConnection(config: TestMailjetRequest): Promise<{ message: string }> {
|
| 201 |
+
const { data } = await apiClient.post<{ message: string }>('/admin/settings/test-mailjet', config)
|
| 202 |
return data
|
| 203 |
}
|
| 204 |
|
|
|
|
| 216 |
smtp_use_tls: boolean
|
| 217 |
}
|
| 218 |
|
| 219 |
+
export interface SendMailjetTestEmailRequest {
|
| 220 |
email: string
|
| 221 |
+
mailjet_api_key: string
|
| 222 |
+
mailjet_secret_key: string
|
| 223 |
+
mailjet_from_email: string
|
| 224 |
+
mailjet_from_name: string
|
| 225 |
}
|
| 226 |
|
| 227 |
/**
|
|
|
|
| 237 |
return data
|
| 238 |
}
|
| 239 |
|
| 240 |
+
export async function sendMailjetTestEmail(
|
| 241 |
+
request: SendMailjetTestEmailRequest
|
| 242 |
): Promise<{ message: string }> {
|
| 243 |
const { data } = await apiClient.post<{ message: string }>(
|
| 244 |
+
'/admin/settings/send-test-email-mailjet',
|
| 245 |
request
|
| 246 |
)
|
| 247 |
return data
|
|
|
|
| 562 |
getSettings,
|
| 563 |
updateSettings,
|
| 564 |
testSmtpConnection,
|
| 565 |
+
testMailjetConnection,
|
| 566 |
sendTestEmail,
|
| 567 |
+
sendMailjetTestEmail,
|
| 568 |
getAdminApiKey,
|
| 569 |
regenerateAdminApiKey,
|
| 570 |
deleteAdminApiKey,
|
frontend/src/i18n/locales/en.ts
CHANGED
|
@@ -4245,33 +4245,38 @@ export default {
|
|
| 4245 |
useTls: 'Use TLS',
|
| 4246 |
useTlsHint: 'Enable TLS encryption for SMTP connection'
|
| 4247 |
},
|
| 4248 |
-
|
| 4249 |
-
title: '
|
| 4250 |
-
description: 'Configure
|
| 4251 |
testConnection: 'Test API Connection',
|
| 4252 |
testing: 'Testing...',
|
| 4253 |
provider: 'Active Email Provider',
|
| 4254 |
providerHint: 'System emails such as verification codes and password reset links use the selected provider',
|
| 4255 |
providerSmtp: 'SMTP',
|
| 4256 |
-
|
| 4257 |
-
apiKey: '
|
| 4258 |
-
apiKeyPlaceholder: '
|
| 4259 |
apiKeyHint: 'Leave empty to keep the existing API key',
|
| 4260 |
-
apiKeyConfiguredPlaceholder: '
|
| 4261 |
apiKeyConfiguredHint: 'API key configured. Leave empty to keep the current value.',
|
| 4262 |
-
|
| 4263 |
-
|
| 4264 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4265 |
fromNamePlaceholder: 'Sub2API',
|
| 4266 |
-
connectionSuccess: '
|
| 4267 |
-
connectionFailed: '
|
| 4268 |
-
testEmailSent: '
|
| 4269 |
-
testEmailFailed: 'Failed to send
|
| 4270 |
},
|
| 4271 |
testEmail: {
|
| 4272 |
title: 'Send Test Email',
|
| 4273 |
description: 'Send a test email to verify your SMTP configuration',
|
| 4274 |
-
|
| 4275 |
recipientEmail: 'Recipient Email',
|
| 4276 |
recipientEmailPlaceholder: "test{'@'}example.com",
|
| 4277 |
sendTestEmail: 'Send Test Email',
|
|
|
|
| 4245 |
useTls: 'Use TLS',
|
| 4246 |
useTlsHint: 'Enable TLS encryption for SMTP connection'
|
| 4247 |
},
|
| 4248 |
+
mailjet: {
|
| 4249 |
+
title: 'Email Verification API Settings',
|
| 4250 |
+
description: 'Configure Mailjet API delivery for verification codes, password reset links, and other system emails',
|
| 4251 |
testConnection: 'Test API Connection',
|
| 4252 |
testing: 'Testing...',
|
| 4253 |
provider: 'Active Email Provider',
|
| 4254 |
providerHint: 'System emails such as verification codes and password reset links use the selected provider',
|
| 4255 |
providerSmtp: 'SMTP',
|
| 4256 |
+
providerMailjet: 'Mailjet API',
|
| 4257 |
+
apiKey: 'Mailjet API Key',
|
| 4258 |
+
apiKeyPlaceholder: 'api_key...',
|
| 4259 |
apiKeyHint: 'Leave empty to keep the existing API key',
|
| 4260 |
+
apiKeyConfiguredPlaceholder: '********************************',
|
| 4261 |
apiKeyConfiguredHint: 'API key configured. Leave empty to keep the current value.',
|
| 4262 |
+
secretKey: 'Mailjet Secret Key',
|
| 4263 |
+
secretKeyPlaceholder: 'secret_key...',
|
| 4264 |
+
secretKeyHint: 'Leave empty to keep the existing secret key',
|
| 4265 |
+
secretKeyConfiguredPlaceholder: '********************************',
|
| 4266 |
+
secretKeyConfiguredHint: 'Secret key configured. Leave empty to keep the current value.',
|
| 4267 |
+
fromEmail: 'Mailjet From Email',
|
| 4268 |
+
fromEmailPlaceholder: "noreply{'@'}example.com",
|
| 4269 |
+
fromName: 'Mailjet From Name',
|
| 4270 |
fromNamePlaceholder: 'Sub2API',
|
| 4271 |
+
connectionSuccess: 'Mailjet API connection successful',
|
| 4272 |
+
connectionFailed: 'Mailjet API connection test failed',
|
| 4273 |
+
testEmailSent: 'Mailjet test email sent successfully',
|
| 4274 |
+
testEmailFailed: 'Failed to send Mailjet test email'
|
| 4275 |
},
|
| 4276 |
testEmail: {
|
| 4277 |
title: 'Send Test Email',
|
| 4278 |
description: 'Send a test email to verify your SMTP configuration',
|
| 4279 |
+
descriptionMailjet: 'Send a test email using the currently selected Mailjet API configuration',
|
| 4280 |
recipientEmail: 'Recipient Email',
|
| 4281 |
recipientEmailPlaceholder: "test{'@'}example.com",
|
| 4282 |
sendTestEmail: 'Send Test Email',
|
frontend/src/i18n/locales/zh.ts
CHANGED
|
@@ -4410,33 +4410,38 @@ export default {
|
|
| 4410 |
useTls: '使用 TLS',
|
| 4411 |
useTlsHint: '为 SMTP 连接启用 TLS 加密'
|
| 4412 |
},
|
| 4413 |
-
|
| 4414 |
-
title: '
|
| 4415 |
-
description: '
|
| 4416 |
testConnection: '测试 API 连接',
|
| 4417 |
testing: '测试中...',
|
| 4418 |
provider: '当前邮件提供商',
|
| 4419 |
providerHint: '验证码、重置密码等系统邮件会使用这里选择的提供商发送',
|
| 4420 |
providerSmtp: 'SMTP',
|
| 4421 |
-
|
| 4422 |
-
apiKey: '
|
| 4423 |
-
apiKeyPlaceholder: '
|
| 4424 |
apiKeyHint: '留空则保留当前 API Key',
|
| 4425 |
-
apiKeyConfiguredPlaceholder: '
|
| 4426 |
apiKeyConfiguredHint: 'API Key 已配置,留空则保留当前值。',
|
| 4427 |
-
|
| 4428 |
-
|
| 4429 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4430 |
fromNamePlaceholder: 'Sub2API',
|
| 4431 |
-
connectionSuccess: '
|
| 4432 |
-
connectionFailed: '
|
| 4433 |
-
testEmailSent: '
|
| 4434 |
-
testEmailFailed: '
|
| 4435 |
},
|
| 4436 |
testEmail: {
|
| 4437 |
title: '发送测试邮件',
|
| 4438 |
description: '发送测试邮件以验证 SMTP 配置',
|
| 4439 |
-
|
| 4440 |
recipientEmail: '收件人邮箱',
|
| 4441 |
recipientEmailPlaceholder: "test{'@'}example.com",
|
| 4442 |
sendTestEmail: '发送测试邮件',
|
|
|
|
| 4410 |
useTls: '使用 TLS',
|
| 4411 |
useTlsHint: '为 SMTP 连接启用 TLS 加密'
|
| 4412 |
},
|
| 4413 |
+
mailjet: {
|
| 4414 |
+
title: '邮箱验证 API 设置',
|
| 4415 |
+
description: '配置 Mailjet API,用于发送验证码、重置密码链接等系统邮件',
|
| 4416 |
testConnection: '测试 API 连接',
|
| 4417 |
testing: '测试中...',
|
| 4418 |
provider: '当前邮件提供商',
|
| 4419 |
providerHint: '验证码、重置密码等系统邮件会使用这里选择的提供商发送',
|
| 4420 |
providerSmtp: 'SMTP',
|
| 4421 |
+
providerMailjet: 'Mailjet API',
|
| 4422 |
+
apiKey: 'Mailjet API Key',
|
| 4423 |
+
apiKeyPlaceholder: 'api_key...',
|
| 4424 |
apiKeyHint: '留空则保留当前 API Key',
|
| 4425 |
+
apiKeyConfiguredPlaceholder: '********************************',
|
| 4426 |
apiKeyConfiguredHint: 'API Key 已配置,留空则保留当前值。',
|
| 4427 |
+
secretKey: 'Mailjet Secret Key',
|
| 4428 |
+
secretKeyPlaceholder: 'secret_key...',
|
| 4429 |
+
secretKeyHint: '留空则保留当前 Secret Key',
|
| 4430 |
+
secretKeyConfiguredPlaceholder: '********************************',
|
| 4431 |
+
secretKeyConfiguredHint: 'Secret Key 已配置,留空则保留当前值。',
|
| 4432 |
+
fromEmail: 'Mailjet 发件邮箱',
|
| 4433 |
+
fromEmailPlaceholder: "noreply{'@'}example.com",
|
| 4434 |
+
fromName: 'Mailjet 发件人名称',
|
| 4435 |
fromNamePlaceholder: 'Sub2API',
|
| 4436 |
+
connectionSuccess: 'Mailjet API 连接成功',
|
| 4437 |
+
connectionFailed: 'Mailjet API 连接测试失败',
|
| 4438 |
+
testEmailSent: 'Mailjet 测试邮件发送成功',
|
| 4439 |
+
testEmailFailed: 'Mailjet 测试邮件发送失败'
|
| 4440 |
},
|
| 4441 |
testEmail: {
|
| 4442 |
title: '发送测试邮件',
|
| 4443 |
description: '发送测试邮件以验证 SMTP 配置',
|
| 4444 |
+
descriptionMailjet: '使用当前选择的 Mailjet API 配置发送测试邮件',
|
| 4445 |
recipientEmail: '收件人邮箱',
|
| 4446 |
recipientEmailPlaceholder: "test{'@'}example.com",
|
| 4447 |
sendTestEmail: '发送测试邮件',
|
frontend/src/views/admin/SettingsView.vue
CHANGED
|
@@ -1712,19 +1712,19 @@
|
|
| 1712 |
>
|
| 1713 |
<div>
|
| 1714 |
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
| 1715 |
-
{{ t('admin.settings.
|
| 1716 |
</h2>
|
| 1717 |
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
| 1718 |
-
{{ t('admin.settings.
|
| 1719 |
</p>
|
| 1720 |
</div>
|
| 1721 |
<button
|
| 1722 |
type="button"
|
| 1723 |
-
@click="
|
| 1724 |
-
:disabled="
|
| 1725 |
class="btn btn-secondary btn-sm"
|
| 1726 |
>
|
| 1727 |
-
<svg v-if="
|
| 1728 |
<circle
|
| 1729 |
class="opacity-25"
|
| 1730 |
cx="12"
|
|
@@ -1740,69 +1740,91 @@
|
|
| 1740 |
></path>
|
| 1741 |
</svg>
|
| 1742 |
{{
|
| 1743 |
-
|
| 1744 |
-
? t('admin.settings.
|
| 1745 |
-
: t('admin.settings.
|
| 1746 |
}}
|
| 1747 |
</button>
|
| 1748 |
</div>
|
| 1749 |
<div class="space-y-6 p-6">
|
| 1750 |
<div>
|
| 1751 |
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 1752 |
-
{{ t('admin.settings.
|
| 1753 |
</label>
|
| 1754 |
<select v-model="form.email_provider" class="input">
|
| 1755 |
-
<option value="smtp">{{ t('admin.settings.
|
| 1756 |
-
<option value="
|
| 1757 |
</select>
|
| 1758 |
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
| 1759 |
-
{{ t('admin.settings.
|
| 1760 |
</p>
|
| 1761 |
</div>
|
| 1762 |
|
| 1763 |
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
| 1764 |
<div class="md:col-span-2">
|
| 1765 |
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 1766 |
-
{{ t('admin.settings.
|
| 1767 |
</label>
|
| 1768 |
<input
|
| 1769 |
-
v-model="form.
|
| 1770 |
type="password"
|
| 1771 |
class="input"
|
| 1772 |
:placeholder="
|
| 1773 |
-
form.
|
| 1774 |
-
? t('admin.settings.
|
| 1775 |
-
: t('admin.settings.
|
| 1776 |
"
|
| 1777 |
/>
|
| 1778 |
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
| 1779 |
{{
|
| 1780 |
-
form.
|
| 1781 |
-
? t('admin.settings.
|
| 1782 |
-
: t('admin.settings.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1783 |
}}
|
| 1784 |
</p>
|
| 1785 |
</div>
|
| 1786 |
<div>
|
| 1787 |
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 1788 |
-
{{ t('admin.settings.
|
| 1789 |
</label>
|
| 1790 |
<input
|
| 1791 |
-
v-model="form.
|
| 1792 |
type="email"
|
| 1793 |
class="input"
|
| 1794 |
-
:placeholder="t('admin.settings.
|
| 1795 |
/>
|
| 1796 |
</div>
|
| 1797 |
<div>
|
| 1798 |
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 1799 |
-
{{ t('admin.settings.
|
| 1800 |
</label>
|
| 1801 |
<input
|
| 1802 |
-
v-model="form.
|
| 1803 |
type="text"
|
| 1804 |
class="input"
|
| 1805 |
-
:placeholder="t('admin.settings.
|
| 1806 |
/>
|
| 1807 |
</div>
|
| 1808 |
</div>
|
|
@@ -1817,8 +1839,8 @@
|
|
| 1817 |
</h2>
|
| 1818 |
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
| 1819 |
{{
|
| 1820 |
-
form.email_provider === '
|
| 1821 |
-
? t('admin.settings.testEmail.
|
| 1822 |
: t('admin.settings.testEmail.description')
|
| 1823 |
}}
|
| 1824 |
</p>
|
|
@@ -1958,7 +1980,7 @@ const { copyToClipboard } = useClipboard()
|
|
| 1958 |
const loading = ref(true)
|
| 1959 |
const saving = ref(false)
|
| 1960 |
const testingSmtp = ref(false)
|
| 1961 |
-
const
|
| 1962 |
const sendingTestEmail = ref(false)
|
| 1963 |
const testEmailAddress = ref('')
|
| 1964 |
const registrationEmailSuffixWhitelistTags = ref<string[]>([])
|
|
@@ -2024,7 +2046,8 @@ interface DefaultSubscriptionGroupOption {
|
|
| 2024 |
|
| 2025 |
type SettingsForm = SystemSettings & {
|
| 2026 |
smtp_password: string
|
| 2027 |
-
|
|
|
|
| 2028 |
turnstile_secret_key: string
|
| 2029 |
linuxdo_connect_client_secret: string
|
| 2030 |
}
|
|
@@ -2064,10 +2087,12 @@ const form = reactive<SettingsForm>({
|
|
| 2064 |
smtp_from_name: '',
|
| 2065 |
smtp_use_tls: true,
|
| 2066 |
email_provider: 'smtp',
|
| 2067 |
-
|
| 2068 |
-
|
| 2069 |
-
|
| 2070 |
-
|
|
|
|
|
|
|
| 2071 |
// Cloudflare Turnstile
|
| 2072 |
turnstile_enabled: false,
|
| 2073 |
turnstile_site_key: '',
|
|
@@ -2247,7 +2272,8 @@ async function loadSettings() {
|
|
| 2247 |
)
|
| 2248 |
registrationEmailSuffixWhitelistDraft.value = ''
|
| 2249 |
form.smtp_password = ''
|
| 2250 |
-
form.
|
|
|
|
| 2251 |
form.turnstile_secret_key = ''
|
| 2252 |
form.linuxdo_connect_client_secret = ''
|
| 2253 |
} catch (error: any) {
|
|
@@ -2348,9 +2374,10 @@ async function saveSettings() {
|
|
| 2348 |
smtp_from_name: form.smtp_from_name,
|
| 2349 |
smtp_use_tls: form.smtp_use_tls,
|
| 2350 |
email_provider: form.email_provider,
|
| 2351 |
-
|
| 2352 |
-
|
| 2353 |
-
|
|
|
|
| 2354 |
turnstile_enabled: form.turnstile_enabled,
|
| 2355 |
turnstile_site_key: form.turnstile_site_key,
|
| 2356 |
turnstile_secret_key: form.turnstile_secret_key || undefined,
|
|
@@ -2376,7 +2403,8 @@ async function saveSettings() {
|
|
| 2376 |
)
|
| 2377 |
registrationEmailSuffixWhitelistDraft.value = ''
|
| 2378 |
form.smtp_password = ''
|
| 2379 |
-
form.
|
|
|
|
| 2380 |
form.turnstile_secret_key = ''
|
| 2381 |
form.linuxdo_connect_client_secret = ''
|
| 2382 |
// Refresh cached settings so sidebar/header update immediately
|
|
@@ -2413,19 +2441,20 @@ async function testSmtpConnection() {
|
|
| 2413 |
}
|
| 2414 |
}
|
| 2415 |
|
| 2416 |
-
async function
|
| 2417 |
-
|
| 2418 |
try {
|
| 2419 |
-
const result = await adminAPI.settings.
|
| 2420 |
-
|
|
|
|
| 2421 |
})
|
| 2422 |
-
appStore.showSuccess(result.message || t('admin.settings.
|
| 2423 |
} catch (error: any) {
|
| 2424 |
appStore.showError(
|
| 2425 |
-
t('admin.settings.
|
| 2426 |
)
|
| 2427 |
} finally {
|
| 2428 |
-
|
| 2429 |
}
|
| 2430 |
}
|
| 2431 |
|
|
@@ -2437,12 +2466,13 @@ async function sendTestEmail() {
|
|
| 2437 |
|
| 2438 |
sendingTestEmail.value = true
|
| 2439 |
try {
|
| 2440 |
-
const result = form.email_provider === '
|
| 2441 |
-
? await adminAPI.settings.
|
| 2442 |
email: testEmailAddress.value,
|
| 2443 |
-
|
| 2444 |
-
|
| 2445 |
-
|
|
|
|
| 2446 |
})
|
| 2447 |
: await adminAPI.settings.sendTestEmail({
|
| 2448 |
email: testEmailAddress.value,
|
|
@@ -2457,14 +2487,14 @@ async function sendTestEmail() {
|
|
| 2457 |
// API returns { message: "..." } on success, errors are thrown as exceptions
|
| 2458 |
appStore.showSuccess(
|
| 2459 |
result.message ||
|
| 2460 |
-
(form.email_provider === '
|
| 2461 |
-
? t('admin.settings.
|
| 2462 |
: t('admin.settings.testEmailSent'))
|
| 2463 |
)
|
| 2464 |
} catch (error: any) {
|
| 2465 |
appStore.showError(
|
| 2466 |
-
(form.email_provider === '
|
| 2467 |
-
? t('admin.settings.
|
| 2468 |
: t('admin.settings.failedToSendTestEmail')) +
|
| 2469 |
': ' +
|
| 2470 |
(error.message || t('common.unknownError'))
|
|
|
|
| 1712 |
>
|
| 1713 |
<div>
|
| 1714 |
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
| 1715 |
+
{{ t('admin.settings.mailjet.title') }}
|
| 1716 |
</h2>
|
| 1717 |
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
| 1718 |
+
{{ t('admin.settings.mailjet.description') }}
|
| 1719 |
</p>
|
| 1720 |
</div>
|
| 1721 |
<button
|
| 1722 |
type="button"
|
| 1723 |
+
@click="testMailjetConnection"
|
| 1724 |
+
:disabled="testingMailjet"
|
| 1725 |
class="btn btn-secondary btn-sm"
|
| 1726 |
>
|
| 1727 |
+
<svg v-if="testingMailjet" class="h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
| 1728 |
<circle
|
| 1729 |
class="opacity-25"
|
| 1730 |
cx="12"
|
|
|
|
| 1740 |
></path>
|
| 1741 |
</svg>
|
| 1742 |
{{
|
| 1743 |
+
testingMailjet
|
| 1744 |
+
? t('admin.settings.mailjet.testing')
|
| 1745 |
+
: t('admin.settings.mailjet.testConnection')
|
| 1746 |
}}
|
| 1747 |
</button>
|
| 1748 |
</div>
|
| 1749 |
<div class="space-y-6 p-6">
|
| 1750 |
<div>
|
| 1751 |
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 1752 |
+
{{ t('admin.settings.mailjet.provider') }}
|
| 1753 |
</label>
|
| 1754 |
<select v-model="form.email_provider" class="input">
|
| 1755 |
+
<option value="smtp">{{ t('admin.settings.mailjet.providerSmtp') }}</option>
|
| 1756 |
+
<option value="mailjet">{{ t('admin.settings.mailjet.providerMailjet') }}</option>
|
| 1757 |
</select>
|
| 1758 |
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
| 1759 |
+
{{ t('admin.settings.mailjet.providerHint') }}
|
| 1760 |
</p>
|
| 1761 |
</div>
|
| 1762 |
|
| 1763 |
<div class="grid grid-cols-1 gap-6 md:grid-cols-2">
|
| 1764 |
<div class="md:col-span-2">
|
| 1765 |
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 1766 |
+
{{ t('admin.settings.mailjet.apiKey') }}
|
| 1767 |
</label>
|
| 1768 |
<input
|
| 1769 |
+
v-model="form.mailjet_api_key"
|
| 1770 |
type="password"
|
| 1771 |
class="input"
|
| 1772 |
:placeholder="
|
| 1773 |
+
form.mailjet_api_key_configured
|
| 1774 |
+
? t('admin.settings.mailjet.apiKeyConfiguredPlaceholder')
|
| 1775 |
+
: t('admin.settings.mailjet.apiKeyPlaceholder')
|
| 1776 |
"
|
| 1777 |
/>
|
| 1778 |
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
| 1779 |
{{
|
| 1780 |
+
form.mailjet_api_key_configured
|
| 1781 |
+
? t('admin.settings.mailjet.apiKeyConfiguredHint')
|
| 1782 |
+
: t('admin.settings.mailjet.apiKeyHint')
|
| 1783 |
+
}}
|
| 1784 |
+
</p>
|
| 1785 |
+
</div>
|
| 1786 |
+
<div class="md:col-span-2">
|
| 1787 |
+
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 1788 |
+
{{ t('admin.settings.mailjet.secretKey') }}
|
| 1789 |
+
</label>
|
| 1790 |
+
<input
|
| 1791 |
+
v-model="form.mailjet_secret_key"
|
| 1792 |
+
type="password"
|
| 1793 |
+
class="input"
|
| 1794 |
+
:placeholder="
|
| 1795 |
+
form.mailjet_secret_key_configured
|
| 1796 |
+
? t('admin.settings.mailjet.secretKeyConfiguredPlaceholder')
|
| 1797 |
+
: t('admin.settings.mailjet.secretKeyPlaceholder')
|
| 1798 |
+
"
|
| 1799 |
+
/>
|
| 1800 |
+
<p class="mt-1.5 text-xs text-gray-500 dark:text-gray-400">
|
| 1801 |
+
{{
|
| 1802 |
+
form.mailjet_secret_key_configured
|
| 1803 |
+
? t('admin.settings.mailjet.secretKeyConfiguredHint')
|
| 1804 |
+
: t('admin.settings.mailjet.secretKeyHint')
|
| 1805 |
}}
|
| 1806 |
</p>
|
| 1807 |
</div>
|
| 1808 |
<div>
|
| 1809 |
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 1810 |
+
{{ t('admin.settings.mailjet.fromEmail') }}
|
| 1811 |
</label>
|
| 1812 |
<input
|
| 1813 |
+
v-model="form.mailjet_from_email"
|
| 1814 |
type="email"
|
| 1815 |
class="input"
|
| 1816 |
+
:placeholder="t('admin.settings.mailjet.fromEmailPlaceholder')"
|
| 1817 |
/>
|
| 1818 |
</div>
|
| 1819 |
<div>
|
| 1820 |
<label class="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
| 1821 |
+
{{ t('admin.settings.mailjet.fromName') }}
|
| 1822 |
</label>
|
| 1823 |
<input
|
| 1824 |
+
v-model="form.mailjet_from_name"
|
| 1825 |
type="text"
|
| 1826 |
class="input"
|
| 1827 |
+
:placeholder="t('admin.settings.mailjet.fromNamePlaceholder')"
|
| 1828 |
/>
|
| 1829 |
</div>
|
| 1830 |
</div>
|
|
|
|
| 1839 |
</h2>
|
| 1840 |
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
| 1841 |
{{
|
| 1842 |
+
form.email_provider === 'mailjet'
|
| 1843 |
+
? t('admin.settings.testEmail.descriptionMailjet')
|
| 1844 |
: t('admin.settings.testEmail.description')
|
| 1845 |
}}
|
| 1846 |
</p>
|
|
|
|
| 1980 |
const loading = ref(true)
|
| 1981 |
const saving = ref(false)
|
| 1982 |
const testingSmtp = ref(false)
|
| 1983 |
+
const testingMailjet = ref(false)
|
| 1984 |
const sendingTestEmail = ref(false)
|
| 1985 |
const testEmailAddress = ref('')
|
| 1986 |
const registrationEmailSuffixWhitelistTags = ref<string[]>([])
|
|
|
|
| 2046 |
|
| 2047 |
type SettingsForm = SystemSettings & {
|
| 2048 |
smtp_password: string
|
| 2049 |
+
mailjet_api_key: string
|
| 2050 |
+
mailjet_secret_key: string
|
| 2051 |
turnstile_secret_key: string
|
| 2052 |
linuxdo_connect_client_secret: string
|
| 2053 |
}
|
|
|
|
| 2087 |
smtp_from_name: '',
|
| 2088 |
smtp_use_tls: true,
|
| 2089 |
email_provider: 'smtp',
|
| 2090 |
+
mailjet_api_key: '',
|
| 2091 |
+
mailjet_secret_key: '',
|
| 2092 |
+
mailjet_api_key_configured: false,
|
| 2093 |
+
mailjet_secret_key_configured: false,
|
| 2094 |
+
mailjet_from_email: '',
|
| 2095 |
+
mailjet_from_name: '',
|
| 2096 |
// Cloudflare Turnstile
|
| 2097 |
turnstile_enabled: false,
|
| 2098 |
turnstile_site_key: '',
|
|
|
|
| 2272 |
)
|
| 2273 |
registrationEmailSuffixWhitelistDraft.value = ''
|
| 2274 |
form.smtp_password = ''
|
| 2275 |
+
form.mailjet_api_key = ''
|
| 2276 |
+
form.mailjet_secret_key = ''
|
| 2277 |
form.turnstile_secret_key = ''
|
| 2278 |
form.linuxdo_connect_client_secret = ''
|
| 2279 |
} catch (error: any) {
|
|
|
|
| 2374 |
smtp_from_name: form.smtp_from_name,
|
| 2375 |
smtp_use_tls: form.smtp_use_tls,
|
| 2376 |
email_provider: form.email_provider,
|
| 2377 |
+
mailjet_api_key: form.mailjet_api_key || undefined,
|
| 2378 |
+
mailjet_secret_key: form.mailjet_secret_key || undefined,
|
| 2379 |
+
mailjet_from_email: form.mailjet_from_email,
|
| 2380 |
+
mailjet_from_name: form.mailjet_from_name,
|
| 2381 |
turnstile_enabled: form.turnstile_enabled,
|
| 2382 |
turnstile_site_key: form.turnstile_site_key,
|
| 2383 |
turnstile_secret_key: form.turnstile_secret_key || undefined,
|
|
|
|
| 2403 |
)
|
| 2404 |
registrationEmailSuffixWhitelistDraft.value = ''
|
| 2405 |
form.smtp_password = ''
|
| 2406 |
+
form.mailjet_api_key = ''
|
| 2407 |
+
form.mailjet_secret_key = ''
|
| 2408 |
form.turnstile_secret_key = ''
|
| 2409 |
form.linuxdo_connect_client_secret = ''
|
| 2410 |
// Refresh cached settings so sidebar/header update immediately
|
|
|
|
| 2441 |
}
|
| 2442 |
}
|
| 2443 |
|
| 2444 |
+
async function testMailjetConnection() {
|
| 2445 |
+
testingMailjet.value = true
|
| 2446 |
try {
|
| 2447 |
+
const result = await adminAPI.settings.testMailjetConnection({
|
| 2448 |
+
mailjet_api_key: form.mailjet_api_key,
|
| 2449 |
+
mailjet_secret_key: form.mailjet_secret_key
|
| 2450 |
})
|
| 2451 |
+
appStore.showSuccess(result.message || t('admin.settings.mailjet.connectionSuccess'))
|
| 2452 |
} catch (error: any) {
|
| 2453 |
appStore.showError(
|
| 2454 |
+
t('admin.settings.mailjet.connectionFailed') + ': ' + (error.message || t('common.unknownError'))
|
| 2455 |
)
|
| 2456 |
} finally {
|
| 2457 |
+
testingMailjet.value = false
|
| 2458 |
}
|
| 2459 |
}
|
| 2460 |
|
|
|
|
| 2466 |
|
| 2467 |
sendingTestEmail.value = true
|
| 2468 |
try {
|
| 2469 |
+
const result = form.email_provider === 'mailjet'
|
| 2470 |
+
? await adminAPI.settings.sendMailjetTestEmail({
|
| 2471 |
email: testEmailAddress.value,
|
| 2472 |
+
mailjet_api_key: form.mailjet_api_key,
|
| 2473 |
+
mailjet_secret_key: form.mailjet_secret_key,
|
| 2474 |
+
mailjet_from_email: form.mailjet_from_email,
|
| 2475 |
+
mailjet_from_name: form.mailjet_from_name
|
| 2476 |
})
|
| 2477 |
: await adminAPI.settings.sendTestEmail({
|
| 2478 |
email: testEmailAddress.value,
|
|
|
|
| 2487 |
// API returns { message: "..." } on success, errors are thrown as exceptions
|
| 2488 |
appStore.showSuccess(
|
| 2489 |
result.message ||
|
| 2490 |
+
(form.email_provider === 'mailjet'
|
| 2491 |
+
? t('admin.settings.mailjet.testEmailSent')
|
| 2492 |
: t('admin.settings.testEmailSent'))
|
| 2493 |
)
|
| 2494 |
} catch (error: any) {
|
| 2495 |
appStore.showError(
|
| 2496 |
+
(form.email_provider === 'mailjet'
|
| 2497 |
+
? t('admin.settings.mailjet.testEmailFailed')
|
| 2498 |
: t('admin.settings.failedToSendTestEmail')) +
|
| 2499 |
': ' +
|
| 2500 |
(error.message || t('common.unknownError'))
|