Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 40 additions & 41 deletions controller/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/logger"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
Expand Down Expand Up @@ -231,18 +232,12 @@ func GetHomePageContent(c *gin.Context) {
func SendEmailVerification(c *gin.Context) {
email := c.Query("email")
if err := common.Validate.Var(email, "required,email"); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
parts := strings.Split(email, "@")
if len(parts) != 2 {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的邮箱地址",
})
common.ApiErrorI18n(c, i18n.MsgAuthEmailInvalid)
return
}
localPart := parts[0]
Expand All @@ -256,37 +251,38 @@ func SendEmailVerification(c *gin.Context) {
}
}
if !allowed {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "The administrator has enabled the email domain name whitelist, and your email address is not allowed due to special symbols or it's not in the whitelist.",
})
common.ApiErrorI18n(c, i18n.MsgAuthEmailDomainRestricted)
return
}
}
if common.EmailAliasRestrictionEnabled {
containsSpecialSymbols := strings.Contains(localPart, "+") || strings.Contains(localPart, ".")
if containsSpecialSymbols {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "管理员已启用邮箱地址别名限制,您的邮箱地址由于包含特殊符号而被拒绝。",
})
common.ApiErrorI18n(c, i18n.MsgAuthEmailAliasRestricted)
return
}
}

if model.IsEmailAlreadyTaken(email) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "邮箱地址已被占用",
})
common.ApiErrorI18n(c, i18n.MsgAuthEmailTaken)
return
}
code := common.GenerateVerificationCode(6)
common.RegisterVerificationCodeWithKey(email, code, common.EmailVerificationPurpose)
subject := fmt.Sprintf("%s邮箱验证邮件", common.SystemName)
content := fmt.Sprintf("<p>您好,你正在进行%s邮箱验证。</p>"+
"<p>您的验证码为: <strong>%s</strong></p>"+
"<p>验证码 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, code, common.VerificationValidMinutes)
subject := i18n.T(c, i18n.MsgAuthEmailVerificationSubject, map[string]any{
"SystemName": common.SystemName,
})
content := fmt.Sprintf("<p>%s</p><p>%s</p><p>%s</p>",
i18n.T(c, i18n.MsgAuthEmailVerificationIntro, map[string]any{
"SystemName": common.SystemName,
}),
i18n.T(c, i18n.MsgAuthEmailVerificationCode, map[string]any{
"Code": code,
}),
i18n.T(c, i18n.MsgAuthEmailVerificationExpiry, map[string]any{
"Minutes": common.VerificationValidMinutes,
}),
)
err := common.SendEmail(subject, email, content)
if err != nil {
common.ApiError(c, err)
Expand All @@ -302,21 +298,30 @@ func SendEmailVerification(c *gin.Context) {
func SendPasswordResetEmail(c *gin.Context) {
email := c.Query("email")
if err := common.Validate.Var(email, "required,email"); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
if model.IsEmailAlreadyTaken(email) {
code := common.GenerateVerificationCode(0)
common.RegisterVerificationCodeWithKey(email, code, common.PasswordResetPurpose)
link := fmt.Sprintf("%s/user/reset?email=%s&token=%s", system_setting.ServerAddress, email, code)
subject := fmt.Sprintf("%s密码重置", common.SystemName)
content := fmt.Sprintf("<p>您好,你正在进行%s密码重置。</p>"+
"<p>点击 <a href='%s'>此处</a> 进行密码重置。</p>"+
"<p>如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> %s </p>"+
"<p>重置链接 %d 分钟内有效,如果不是本人操作,请忽略。</p>", common.SystemName, link, link, common.VerificationValidMinutes)
subject := i18n.T(c, i18n.MsgAuthPasswordResetSubject, map[string]any{
"SystemName": common.SystemName,
})
content := fmt.Sprintf("<p>%s</p><p>%s</p><p>%s</p><p>%s</p>",
i18n.T(c, i18n.MsgAuthPasswordResetIntro, map[string]any{
"SystemName": common.SystemName,
}),
i18n.T(c, i18n.MsgAuthPasswordResetClick, map[string]any{
"Link": link,
}),
i18n.T(c, i18n.MsgAuthPasswordResetCopy, map[string]any{
"Link": link,
}),
i18n.T(c, i18n.MsgAuthPasswordResetExpiry, map[string]any{
"Minutes": common.VerificationValidMinutes,
}),
)
err := common.SendEmail(subject, email, content)
if err != nil {
logger.LogError(c.Request.Context(), fmt.Sprintf("failed to send password reset email to %s: %s", email, err.Error()))
Expand All @@ -337,17 +342,11 @@ func ResetPassword(c *gin.Context) {
var req PasswordResetRequest
err := json.NewDecoder(c.Request.Body).Decode(&req)
if req.Email == "" || req.Token == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无效的参数",
})
common.ApiErrorI18n(c, i18n.MsgInvalidParams)
return
}
if !common.VerifyCodeWithKey(req.Email, req.Token, common.PasswordResetPurpose) {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "重置链接非法或已过期",
})
common.ApiErrorI18n(c, i18n.MsgAuthPasswordResetInvalid)
return
}
password := common.GenerateVerificationCode(12)
Expand Down
22 changes: 22 additions & 0 deletions i18n/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,28 @@ const (
MsgAuthInsufficientPrivilege = "auth.insufficient_privilege"
)

// Public auth and verification messages
const (
MsgAuthEmailInvalid = "auth.email_invalid"
MsgAuthEmailDomainRestricted = "auth.email_domain_restricted"
MsgAuthEmailAliasRestricted = "auth.email_alias_restricted"
MsgAuthEmailTaken = "auth.email_taken"
MsgAuthEmailVerificationSubject = "auth.email_verification_subject"
MsgAuthEmailVerificationIntro = "auth.email_verification_intro"
MsgAuthEmailVerificationCode = "auth.email_verification_code"
MsgAuthEmailVerificationExpiry = "auth.email_verification_expiry"
MsgAuthPasswordResetSubject = "auth.password_reset_subject"
MsgAuthPasswordResetIntro = "auth.password_reset_intro"
MsgAuthPasswordResetClick = "auth.password_reset_click"
MsgAuthPasswordResetCopy = "auth.password_reset_copy"
MsgAuthPasswordResetExpiry = "auth.password_reset_expiry"
MsgAuthPasswordResetInvalid = "auth.password_reset_invalid"
MsgAuthEmailRateLimitWait = "auth.email_rate_limit_wait"
MsgAuthEmailRateLimitRetry = "auth.email_rate_limit_retry"
MsgAuthTurnstileTokenEmpty = "auth.turnstile_token_empty"
MsgAuthTurnstileFailed = "auth.turnstile_failed"
)

// Token related messages
const (
MsgTokenNameTooLong = "token.name_too_long"
Expand Down
18 changes: 18 additions & 0 deletions i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@ auth.user_id_format_error: "Unauthorized, New-Api-User header format error"
auth.user_id_mismatch: "Unauthorized, New-Api-User does not match logged in user"
auth.user_banned: "User has been banned"
auth.insufficient_privilege: "Unauthorized, insufficient privileges"
auth.email_invalid: "Invalid email address"
auth.email_domain_restricted: "The administrator has enabled the email domain whitelist, and your email address is not allowed because it contains special symbols or is not in the whitelist."
auth.email_alias_restricted: "The administrator has enabled email alias restrictions, and your email address was rejected because it contains special symbols."
auth.email_taken: "Email address is already in use"
auth.email_verification_subject: "{{.SystemName}} email verification"
auth.email_verification_intro: "Hello, you are verifying your email address for {{.SystemName}}."
auth.email_verification_code: "Your verification code is: <strong>{{.Code}}</strong>"
auth.email_verification_expiry: "This code is valid for {{.Minutes}} minutes. If you did not request this, you can ignore this email."
auth.password_reset_subject: "{{.SystemName}} password reset"
auth.password_reset_intro: "Hello, you requested a password reset for {{.SystemName}}."
auth.password_reset_click: "Click <a href='{{.Link}}'>here</a> to reset your password."
auth.password_reset_copy: "If the link is not clickable, copy and paste this URL into your browser:<br> {{.Link}}"
auth.password_reset_expiry: "This reset link is valid for {{.Minutes}} minutes. If you did not request this, you can ignore this email."
auth.password_reset_invalid: "The reset link is invalid or has expired"
auth.email_rate_limit_wait: "Too many requests. Please wait {{.Seconds}} seconds and try again"
auth.email_rate_limit_retry: "Too many requests. Please try again later"
auth.turnstile_token_empty: "Turnstile token is empty"
auth.turnstile_failed: "Turnstile verification failed. Please refresh and try again."

# Token messages
token.name_too_long: "Token name is too long"
Expand Down
18 changes: 18 additions & 0 deletions i18n/locales/zh-CN.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ auth.user_id_format_error: "无权进行此操作,New-Api-User 格式错误"
auth.user_id_mismatch: "无权进行此操作,New-Api-User 与登录用户不匹配"
auth.user_banned: "用户已被封禁"
auth.insufficient_privilege: "无权进行此操作,权限不足"
auth.email_invalid: "无效的邮箱地址"
auth.email_domain_restricted: "管理员已启用邮箱域名白名单,您的邮箱地址由于包含特殊符号或不在白名单中而被拒绝。"
auth.email_alias_restricted: "管理员已启用邮箱地址别名限制,您的邮箱地址由于包含特殊符号而被拒绝。"
auth.email_taken: "邮箱地址已被占用"
auth.email_verification_subject: "{{.SystemName}}邮箱验证邮件"
auth.email_verification_intro: "您好,你正在进行{{.SystemName}}邮箱验证。"
auth.email_verification_code: "您的验证码为: <strong>{{.Code}}</strong>"
auth.email_verification_expiry: "验证码 {{.Minutes}} 分钟内有效,如果不是本人操作,请忽略。"
auth.password_reset_subject: "{{.SystemName}}密码重置"
auth.password_reset_intro: "您好,你正在进行{{.SystemName}}密码重置。"
auth.password_reset_click: "点击 <a href='{{.Link}}'>此处</a> 进行密码重置。"
auth.password_reset_copy: "如果链接无法点击,请尝试点击下面的链接或将其复制到浏览器中打开:<br> {{.Link}}"
auth.password_reset_expiry: "重置链接 {{.Minutes}} 分钟内有效,如果不是本人操作,请忽略。"
auth.password_reset_invalid: "重置链接非法或已过期"
auth.email_rate_limit_wait: "发送过于频繁,请等待 {{.Seconds}} 秒后再试"
auth.email_rate_limit_retry: "发送过于频繁,请稍后再试"
auth.turnstile_token_empty: "Turnstile token 为空"
auth.turnstile_failed: "Turnstile 校验失败,请刷新重试!"

# Token messages
token.name_too_long: "令牌名称过长"
Expand Down
18 changes: 18 additions & 0 deletions i18n/locales/zh-TW.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,24 @@ auth.user_id_format_error: "無權進行此操作,New-Api-User 格式錯誤"
auth.user_id_mismatch: "無權進行此操作,New-Api-User 與登入使用者不匹配"
auth.user_banned: "使用者已被封禁"
auth.insufficient_privilege: "無權進行此操作,權限不足"
auth.email_invalid: "無效的信箱位址"
auth.email_domain_restricted: "管理員已啟用信箱網域白名單,您的信箱位址由於包含特殊符號或不在白名單中而被拒絕。"
auth.email_alias_restricted: "管理員已啟用信箱位址別名限制,您的信箱位址由於包含特殊符號而被拒絕。"
auth.email_taken: "信箱位址已被占用"
auth.email_verification_subject: "{{.SystemName}}信箱驗證郵件"
auth.email_verification_intro: "您好,您正在進行{{.SystemName}}信箱驗證。"
auth.email_verification_code: "您的驗證碼為: <strong>{{.Code}}</strong>"
auth.email_verification_expiry: "驗證碼 {{.Minutes}} 分鐘內有效,如果不是本人操作,請忽略。"
auth.password_reset_subject: "{{.SystemName}}密碼重設"
auth.password_reset_intro: "您好,您正在進行{{.SystemName}}密碼重設。"
auth.password_reset_click: "點擊 <a href='{{.Link}}'>此處</a> 進行密碼重設。"
auth.password_reset_copy: "如果連結無法點擊,請嘗試點擊下面的連結或將其複製到瀏覽器中開啟:<br> {{.Link}}"
auth.password_reset_expiry: "重設連結 {{.Minutes}} 分鐘內有效,如果不是本人操作,請忽略。"
auth.password_reset_invalid: "重設連結非法或已過期"
auth.email_rate_limit_wait: "傳送過於頻繁,請等待 {{.Seconds}} 秒後再試"
auth.email_rate_limit_retry: "傳送過於頻繁,請稍後再試"
auth.turnstile_token_empty: "Turnstile token 為空"
auth.turnstile_failed: "Turnstile 驗證失敗,請重新整理後再試!"

# Token messages
token.name_too_long: "令牌名稱過長"
Expand Down
8 changes: 5 additions & 3 deletions middleware/email-verification-rate-limit.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package middleware

import (
"context"
"fmt"
"net/http"
"time"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/i18n"

"github.com/gin-gonic/gin"
)
Expand Down Expand Up @@ -49,7 +49,9 @@ func redisEmailVerificationRateLimiter(c *gin.Context) {

c.JSON(http.StatusTooManyRequests, gin.H{
"success": false,
"message": fmt.Sprintf("发送过于频繁,请等待 %d 秒后再试", waitSeconds),
"message": common.TranslateMessage(c, i18n.MsgAuthEmailRateLimitWait, map[string]any{
"Seconds": waitSeconds,
}),
})
c.Abort()
}
Expand All @@ -60,7 +62,7 @@ func memoryEmailVerificationRateLimiter(c *gin.Context) {
if !inMemoryRateLimiter.Request(key, EmailVerificationMaxRequests, EmailVerificationDuration) {
c.JSON(http.StatusTooManyRequests, gin.H{
"success": false,
"message": "发送过于频繁,请稍后再试",
"message": common.TranslateMessage(c, i18n.MsgAuthEmailRateLimitRetry),
})
c.Abort()
return
Expand Down
7 changes: 4 additions & 3 deletions middleware/turnstile-check.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/url"

"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/i18n"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
Expand All @@ -27,7 +28,7 @@ func TurnstileCheck() gin.HandlerFunc {
if response == "" {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "Turnstile token 为空",
"message": common.TranslateMessage(c, i18n.MsgAuthTurnstileTokenEmpty),
})
c.Abort()
return
Expand Down Expand Up @@ -61,7 +62,7 @@ func TurnstileCheck() gin.HandlerFunc {
if !res.Success {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "Turnstile 校验失败,请刷新重试!",
"message": common.TranslateMessage(c, i18n.MsgAuthTurnstileFailed),
})
c.Abort()
return
Expand All @@ -70,7 +71,7 @@ func TurnstileCheck() gin.HandlerFunc {
err = session.Save()
if err != nil {
c.JSON(http.StatusOK, gin.H{
"message": "无法保存会话信息,请重试",
"message": common.TranslateMessage(c, i18n.MsgUserSessionSaveFailed),
"success": false,
})
return
Expand Down