Files
Calcium-Ion 6461f084ff feat(notify): turn each kind of notification on or off (#7868)
SMTP settings now only decide whether mail can be sent. Verification
codes, password reset, email binding and account security mails depend
on SMTP alone, and no notification switch affects them.

Admins get a site-wide low balance reminder switch, on by default, next
to the SMTP settings. The default threshold is entered as an amount.

Users choose which notifications they receive. Low balance reminders are
on for everyone by default. Admins can also turn channel notifications
and upstream model update notices on or off. The root user receives
channel notifications until it turns them off. Other admins receive them
after they turn them on.

A low balance reminder is sent once, when a charge takes the balance
below the threshold. A top-up above the threshold arms it again.

Channel notifications reach every admin who has them on, in that admin's
language. Email binding codes and account security mails follow the
account's language.

Saving notification settings no longer clears the language, the sidebar
layout or the billing preference.

Existing settings keep their behavior, and old clients that do not send
the new fields keep the stored switches. Webhook, Bark and Gotify
payloads are unchanged.
2026-10-17 22:17:59 +08:00

169 lines
4.5 KiB
Go

package controller
import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/i18n"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type wechatLoginResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Data string `json:"data"`
}
func getWeChatIdByCode(code string) (string, error) {
if code == "" {
return "", common.NewMessage("Invalid parameters")
}
req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/wechat/user?code=%s", common.WeChatServerAddress, url.QueryEscape(code)), nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", common.WeChatServerToken)
client := http.Client{
Timeout: 5 * time.Second,
}
httpResponse, err := client.Do(req)
if err != nil {
return "", err
}
defer httpResponse.Body.Close()
var res wechatLoginResponse
err = common.DecodeJson(httpResponse.Body, &res)
if err != nil {
return "", err
}
if !res.Success {
return "", errors.New(res.Message)
}
if res.Data == "" {
return "", common.NewMessage("Verification code is incorrect or has expired")
}
return res.Data, nil
}
func WeChatAuth(c *gin.Context) {
if !common.WeChatAuthEnabled {
common.ApiErrorT(c, "{{provider}} login and registration has not been enabled by administrator", providerParams("WeChat"))
return
}
code := c.Query("code")
wechatId, err := getWeChatIdByCode(code)
if err != nil {
common.ApiError(c, err)
return
}
user := model.User{
WeChatId: wechatId,
}
if model.IsWeChatIdAlreadyTaken(wechatId) {
err := user.FillUserByWeChatId()
if err != nil {
common.ApiError(c, err)
return
}
if user.Id == 0 {
common.ApiErrorT(c, "User has been deleted")
return
}
} else {
if common.RegisterEnabled {
user.Username = "wechat_" + strconv.Itoa(model.GetMaxUserId()+1)
user.DisplayName = "WeChat User"
user.Role = common.RoleCommonUser
user.Status = common.UserStatusEnabled
if err := user.Insert(0); err != nil {
common.ApiError(c, err)
return
}
} else {
common.ApiErrorT(c, "New user registration has been disabled by administrator")
return
}
}
if user.Status != common.UserStatusEnabled {
common.ApiErrorT(c, "User has been banned")
return
}
setupLogin(&user, nil, c)
}
type wechatBindRequest struct {
Code string `json:"code"`
}
func WeChatBind(c *gin.Context) {
identity, ok := middleware.GetStepUpIdentity(c)
if !ok {
writeSecurityOperationError(c, service.ErrAuthTokenInvalid)
return
}
succeeded, notificationFailed := false, false
defer func() {
recordUserSecurityAudit(c, identity.UserID, "user.binding_bind", map[string]any{"provider": "wechat", "success": succeeded, "notification_failed": notificationFailed})
}()
if !common.WeChatAuthEnabled {
common.ApiErrorT(c, "{{provider}} login and registration has not been enabled by administrator", providerParams("WeChat"))
return
}
var req wechatBindRequest
if err := common.DecodeJson(c.Request.Body, &req); err != nil {
common.ApiErrorT(c, "Invalid request")
return
}
code := strings.TrimSpace(req.Code)
context, err := common.Marshal(service.AccountBindingContext{Provider: "wechat", Code: code})
if err != nil {
writeSecurityOperationError(c, err)
return
}
if middleware.RequireSecurityProof(c, service.VerificationOperation{Scope: service.VerificationScopeAccountBind, Context: context}) == nil {
return
}
wechatId, err := getWeChatIdByCode(code)
if err != nil {
common.ApiError(c, err)
return
}
if model.IsWeChatIdAlreadyTaken(wechatId) {
common.ApiErrorT(c, "This {{provider}} account has already been bound", providerParams("WeChat"))
return
}
// 只更新绑定列,避免完整用户快照覆盖并发的封禁、降权或分组变更。
if err := model.DB.Transaction(func(tx *gorm.DB) error {
return model.UpdateUserBindColumnForSessionWithTx(tx, identity, "wechat_id", wechatId)
}); err != nil {
writeSecurityOperationError(c, err)
return
}
succeeded = true
user, err := model.GetUserById(identity.UserID, false)
if err != nil {
writeSecurityOperationError(c, err)
return
}
notificationFailed = service.NotifyAccountSecurityChange(service.AccountMailLanguage(c, user.Id), user.Email, i18n.MsgSecurityEventWeChatLinked) != nil
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
"data": gin.H{"notification_warning": notificationFailed},
})
return
}