mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-19 07:47:28 +00:00
feat: 关联 discord 账号
This commit is contained in:
@@ -193,6 +193,7 @@ docker run --name new-api -d --restart always \
|
|||||||
|
|
||||||
### 🔐 Authorization and Security
|
### 🔐 Authorization and Security
|
||||||
|
|
||||||
|
- 😈 Discord authorization login
|
||||||
- 🤖 LinuxDO authorization login
|
- 🤖 LinuxDO authorization login
|
||||||
- 📱 Telegram authorization login
|
- 📱 Telegram authorization login
|
||||||
- 🔑 OIDC unified authentication
|
- 🔑 OIDC unified authentication
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ docker run --name new-api -d --restart always \
|
|||||||
|
|
||||||
### 🔐 授权与安全
|
### 🔐 授权与安全
|
||||||
|
|
||||||
|
- 😈 Discord 授权登录
|
||||||
- 🤖 LinuxDO 授权登录
|
- 🤖 LinuxDO 授权登录
|
||||||
- 📱 Telegram 授权登录
|
- 📱 Telegram 授权登录
|
||||||
- 🔑 OIDC 统一认证
|
- 🔑 OIDC 统一认证
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ var PasswordLoginEnabled = true
|
|||||||
var PasswordRegisterEnabled = true
|
var PasswordRegisterEnabled = true
|
||||||
var EmailVerificationEnabled = false
|
var EmailVerificationEnabled = false
|
||||||
var GitHubOAuthEnabled = false
|
var GitHubOAuthEnabled = false
|
||||||
|
var DiscordOAuthEnabled = false
|
||||||
var LinuxDOOAuthEnabled = false
|
var LinuxDOOAuthEnabled = false
|
||||||
var WeChatAuthEnabled = false
|
var WeChatAuthEnabled = false
|
||||||
var TelegramOAuthEnabled = false
|
var TelegramOAuthEnabled = false
|
||||||
@@ -82,6 +83,8 @@ var SMTPToken = ""
|
|||||||
|
|
||||||
var GitHubClientId = ""
|
var GitHubClientId = ""
|
||||||
var GitHubClientSecret = ""
|
var GitHubClientSecret = ""
|
||||||
|
var DiscordClientId = ""
|
||||||
|
var DiscordClientSecret = ""
|
||||||
var LinuxDOClientId = ""
|
var LinuxDOClientId = ""
|
||||||
var LinuxDOClientSecret = ""
|
var LinuxDOClientSecret = ""
|
||||||
var LinuxDOMinimumTrustLevel = 0
|
var LinuxDOMinimumTrustLevel = 0
|
||||||
|
|||||||
224
controller/discord.go
Normal file
224
controller/discord.go
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
"github.com/QuantumNous/new-api/model"
|
||||||
|
"github.com/QuantumNous/new-api/setting/system_setting"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscordResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
IDToken string `json:"id_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscordUser struct {
|
||||||
|
UID string `json:"id"`
|
||||||
|
ID string `json:"username"`
|
||||||
|
Name string `json:"global_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDiscordUserInfoByCode(code string) (*DiscordUser, error) {
|
||||||
|
if code == "" {
|
||||||
|
return nil, errors.New("无效的参数")
|
||||||
|
}
|
||||||
|
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("client_id", common.DiscordClientId)
|
||||||
|
values.Set("client_secret", common.DiscordClientSecret)
|
||||||
|
values.Set("code", code)
|
||||||
|
values.Set("grant_type", "authorization_code")
|
||||||
|
values.Set("redirect_uri", fmt.Sprintf("%s/oauth/discord", system_setting.ServerAddress))
|
||||||
|
formData := values.Encode()
|
||||||
|
req, err := http.NewRequest("POST", "https://discord.com/api/v10/oauth2/token", strings.NewReader(formData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至 Discord 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
var discordResponse DiscordResponse
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&discordResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if discordResponse.AccessToken == "" {
|
||||||
|
common.SysError("Discord 获取 Token 失败,请检查设置!")
|
||||||
|
return nil, errors.New("Discord 获取 Token 失败,请检查设置!")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = http.NewRequest("GET", "https://discord.com/api/v10/users/@me", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+discordResponse.AccessToken)
|
||||||
|
res2, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至 Discord 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res2.Body.Close()
|
||||||
|
if res2.StatusCode != http.StatusOK {
|
||||||
|
common.SysError("Discord 获取用户信息失败!请检查设置!")
|
||||||
|
return nil, errors.New("Discord 获取用户信息失败!请检查设置!")
|
||||||
|
}
|
||||||
|
|
||||||
|
var discordUser DiscordUser
|
||||||
|
err = json.NewDecoder(res2.Body).Decode(&discordUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if discordUser.UID == "" || discordUser.ID == "" {
|
||||||
|
common.SysError("Discord 获取用户信息为空!请检查设置!")
|
||||||
|
return nil, errors.New("Discord 获取用户信息为空!请检查设置!")
|
||||||
|
}
|
||||||
|
return &discordUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DiscordOAuth(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
state := c.Query("state")
|
||||||
|
if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "state is empty or not same",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := session.Get("username")
|
||||||
|
if username != nil {
|
||||||
|
DiscordBind(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !common.DiscordOAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 discord 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
discordUser, err := getDiscordUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
DiscordId: discordUser.UID,
|
||||||
|
}
|
||||||
|
if model.IsDiscordIdAlreadyTaken(user.DiscordId) {
|
||||||
|
err := user.FillUserByDiscordId()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if common.RegisterEnabled {
|
||||||
|
if discordUser.ID != "" {
|
||||||
|
user.Username = discordUser.ID
|
||||||
|
} else {
|
||||||
|
user.Username = "discord_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
}
|
||||||
|
if discordUser.Name != "" {
|
||||||
|
user.DisplayName = discordUser.Name
|
||||||
|
} else {
|
||||||
|
user.DisplayName = "Discord User"
|
||||||
|
}
|
||||||
|
err := user.Insert(0)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员关闭了新用户注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status != common.UserStatusEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"message": "用户已被封禁",
|
||||||
|
"success": false,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setupLogin(&user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DiscordBind(c *gin.Context) {
|
||||||
|
if !common.DiscordOAuthEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 Discord 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
discordUser, err := getDiscordUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
DiscordId: discordUser.UID,
|
||||||
|
}
|
||||||
|
if model.IsDiscordIdAlreadyTaken(user.DiscordId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该 Discord 账户已被绑定",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session := sessions.Default(c)
|
||||||
|
id := session.Get("id")
|
||||||
|
// id := c.GetInt("id") // critical bug!
|
||||||
|
user.Id = id.(int)
|
||||||
|
err = user.FillUserById()
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.DiscordId = discordUser.UID
|
||||||
|
err = user.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
common.ApiError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "bind",
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -52,6 +52,8 @@ func GetStatus(c *gin.Context) {
|
|||||||
"email_verification": common.EmailVerificationEnabled,
|
"email_verification": common.EmailVerificationEnabled,
|
||||||
"github_oauth": common.GitHubOAuthEnabled,
|
"github_oauth": common.GitHubOAuthEnabled,
|
||||||
"github_client_id": common.GitHubClientId,
|
"github_client_id": common.GitHubClientId,
|
||||||
|
"discord_oauth": common.DiscordOAuthEnabled,
|
||||||
|
"discord_client_id": common.DiscordClientId,
|
||||||
"linuxdo_oauth": common.LinuxDOOAuthEnabled,
|
"linuxdo_oauth": common.LinuxDOOAuthEnabled,
|
||||||
"linuxdo_client_id": common.LinuxDOClientId,
|
"linuxdo_client_id": common.LinuxDOClientId,
|
||||||
"linuxdo_minimum_trust_level": common.LinuxDOMinimumTrustLevel,
|
"linuxdo_minimum_trust_level": common.LinuxDOMinimumTrustLevel,
|
||||||
|
|||||||
@@ -71,6 +71,14 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "DiscordOAuthEnabled":
|
||||||
|
if option.Value == "true" && common.DiscordClientId == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法启用 Discord OAuth,请先填入 Discord Client Id 以及 Discord Client Secret!",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
case "oidc.enabled":
|
case "oidc.enabled":
|
||||||
if option.Value == "true" && system_setting.GetOIDCSettings().ClientId == "" {
|
if option.Value == "true" && system_setting.GetOIDCSettings().ClientId == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
@@ -453,6 +453,7 @@ func GetSelf(c *gin.Context) {
|
|||||||
"status": user.Status,
|
"status": user.Status,
|
||||||
"email": user.Email,
|
"email": user.Email,
|
||||||
"github_id": user.GitHubId,
|
"github_id": user.GitHubId,
|
||||||
|
"discord_id": user.DiscordId,
|
||||||
"oidc_id": user.OidcId,
|
"oidc_id": user.OidcId,
|
||||||
"wechat_id": user.WeChatId,
|
"wechat_id": user.WeChatId,
|
||||||
"telegram_id": user.TelegramId,
|
"telegram_id": user.TelegramId,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
| 方法 | 路径 | 鉴权 | 说明 |
|
| 方法 | 路径 | 鉴权 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| GET | /api/oauth/github | 公开 | GitHub OAuth 跳转 |
|
| GET | /api/oauth/github | 公开 | GitHub OAuth 跳转 |
|
||||||
|
| GET | /api/oauth/discord | 公开 | Discord 通用 OAuth 跳转 |
|
||||||
| GET | /api/oauth/oidc | 公开 | OIDC 通用 OAuth 跳转 |
|
| GET | /api/oauth/oidc | 公开 | OIDC 通用 OAuth 跳转 |
|
||||||
| GET | /api/oauth/linuxdo | 公开 | LinuxDo OAuth 跳转 |
|
| GET | /api/oauth/linuxdo | 公开 | LinuxDo OAuth 跳转 |
|
||||||
| GET | /api/oauth/wechat | 公开 | 微信扫码登录跳转 |
|
| GET | /api/oauth/wechat | 公开 | 微信扫码登录跳转 |
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled)
|
common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled)
|
||||||
common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled)
|
common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled)
|
||||||
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
|
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
|
||||||
|
common.OptionMap["DiscordOAuthEnabled"] = strconv.FormatBool(common.DiscordOAuthEnabled)
|
||||||
common.OptionMap["LinuxDOOAuthEnabled"] = strconv.FormatBool(common.LinuxDOOAuthEnabled)
|
common.OptionMap["LinuxDOOAuthEnabled"] = strconv.FormatBool(common.LinuxDOOAuthEnabled)
|
||||||
common.OptionMap["TelegramOAuthEnabled"] = strconv.FormatBool(common.TelegramOAuthEnabled)
|
common.OptionMap["TelegramOAuthEnabled"] = strconv.FormatBool(common.TelegramOAuthEnabled)
|
||||||
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
||||||
@@ -95,6 +96,8 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["PayMethods"] = operation_setting.PayMethods2JsonString()
|
common.OptionMap["PayMethods"] = operation_setting.PayMethods2JsonString()
|
||||||
common.OptionMap["GitHubClientId"] = ""
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
common.OptionMap["GitHubClientSecret"] = ""
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
|
common.OptionMap["DiscordClientId"] = ""
|
||||||
|
common.OptionMap["DiscordClientSecret"] = ""
|
||||||
common.OptionMap["TelegramBotToken"] = ""
|
common.OptionMap["TelegramBotToken"] = ""
|
||||||
common.OptionMap["TelegramBotName"] = ""
|
common.OptionMap["TelegramBotName"] = ""
|
||||||
common.OptionMap["WeChatServerAddress"] = ""
|
common.OptionMap["WeChatServerAddress"] = ""
|
||||||
@@ -224,6 +227,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.EmailVerificationEnabled = boolValue
|
common.EmailVerificationEnabled = boolValue
|
||||||
case "GitHubOAuthEnabled":
|
case "GitHubOAuthEnabled":
|
||||||
common.GitHubOAuthEnabled = boolValue
|
common.GitHubOAuthEnabled = boolValue
|
||||||
|
case "DiscordOAuthEnabled":
|
||||||
|
common.DiscordOAuthEnabled = boolValue
|
||||||
case "LinuxDOOAuthEnabled":
|
case "LinuxDOOAuthEnabled":
|
||||||
common.LinuxDOOAuthEnabled = boolValue
|
common.LinuxDOOAuthEnabled = boolValue
|
||||||
case "WeChatAuthEnabled":
|
case "WeChatAuthEnabled":
|
||||||
@@ -360,6 +365,10 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.GitHubClientId = value
|
common.GitHubClientId = value
|
||||||
case "GitHubClientSecret":
|
case "GitHubClientSecret":
|
||||||
common.GitHubClientSecret = value
|
common.GitHubClientSecret = value
|
||||||
|
case "DiscordClientId":
|
||||||
|
common.DiscordClientId = value
|
||||||
|
case "DiscordClientSecret":
|
||||||
|
common.DiscordClientSecret = value
|
||||||
case "LinuxDOClientId":
|
case "LinuxDOClientId":
|
||||||
common.LinuxDOClientId = value
|
common.LinuxDOClientId = value
|
||||||
case "LinuxDOClientSecret":
|
case "LinuxDOClientSecret":
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type User struct {
|
|||||||
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
|
Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled
|
||||||
Email string `json:"email" gorm:"index" validate:"max=50"`
|
Email string `json:"email" gorm:"index" validate:"max=50"`
|
||||||
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
|
GitHubId string `json:"github_id" gorm:"column:github_id;index"`
|
||||||
|
DiscordId string `json:"discord_id" gorm:"column:discord_id;index"`
|
||||||
OidcId string `json:"oidc_id" gorm:"column:oidc_id;index"`
|
OidcId string `json:"oidc_id" gorm:"column:oidc_id;index"`
|
||||||
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
||||||
TelegramId string `json:"telegram_id" gorm:"column:telegram_id;index"`
|
TelegramId string `json:"telegram_id" gorm:"column:telegram_id;index"`
|
||||||
@@ -539,6 +540,14 @@ func (user *User) FillUserByGitHubId() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByDiscordId() error {
|
||||||
|
if user.DiscordId == "" {
|
||||||
|
return errors.New("discord id 为空!")
|
||||||
|
}
|
||||||
|
DB.Where(User{DiscordId: user.DiscordId}).First(user)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) FillUserByOidcId() error {
|
func (user *User) FillUserByOidcId() error {
|
||||||
if user.OidcId == "" {
|
if user.OidcId == "" {
|
||||||
return errors.New("oidc id 为空!")
|
return errors.New("oidc id 为空!")
|
||||||
@@ -578,6 +587,10 @@ func IsGitHubIdAlreadyTaken(githubId string) bool {
|
|||||||
return DB.Unscoped().Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
return DB.Unscoped().Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsDiscordIdAlreadyTaken(discordId string) bool {
|
||||||
|
return DB.Where("discord_id = ?", discordId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
func IsOidcIdAlreadyTaken(oidcId string) bool {
|
func IsOidcIdAlreadyTaken(oidcId string) bool {
|
||||||
return DB.Where("oidc_id = ?", oidcId).Find(&User{}).RowsAffected == 1
|
return DB.Where("oidc_id = ?", oidcId).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||||
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
|
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
|
||||||
|
apiRouter.GET("/oauth/discord", middleware.CriticalRateLimit(), controller.DiscordOAuth)
|
||||||
apiRouter.GET("/oauth/oidc", middleware.CriticalRateLimit(), controller.OidcAuth)
|
apiRouter.GET("/oauth/oidc", middleware.CriticalRateLimit(), controller.OidcAuth)
|
||||||
apiRouter.GET("/oauth/linuxdo", middleware.CriticalRateLimit(), controller.LinuxdoOAuth)
|
apiRouter.GET("/oauth/linuxdo", middleware.CriticalRateLimit(), controller.LinuxdoOAuth)
|
||||||
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
|
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
|
||||||
|
|||||||
@@ -192,6 +192,14 @@ function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/oauth/discord'
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>} key={location.pathname}>
|
||||||
|
<OAuth2Callback type='discord'></OAuth2Callback>
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/oauth/oidc'
|
path='/oauth/oidc'
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
getSystemName,
|
getSystemName,
|
||||||
setUserData,
|
setUserData,
|
||||||
onGitHubOAuthClicked,
|
onGitHubOAuthClicked,
|
||||||
|
onDiscordOAuthClicked,
|
||||||
onOIDCClicked,
|
onOIDCClicked,
|
||||||
onLinuxDOOAuthClicked,
|
onLinuxDOOAuthClicked,
|
||||||
prepareCredentialRequestOptions,
|
prepareCredentialRequestOptions,
|
||||||
@@ -53,6 +54,7 @@ import WeChatIcon from '../common/logo/WeChatIcon';
|
|||||||
import LinuxDoIcon from '../common/logo/LinuxDoIcon';
|
import LinuxDoIcon from '../common/logo/LinuxDoIcon';
|
||||||
import TwoFAVerification from './TwoFAVerification';
|
import TwoFAVerification from './TwoFAVerification';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SiDiscord }from 'react-icons/si';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
@@ -73,6 +75,7 @@ const LoginForm = () => {
|
|||||||
const [showEmailLogin, setShowEmailLogin] = useState(false);
|
const [showEmailLogin, setShowEmailLogin] = useState(false);
|
||||||
const [wechatLoading, setWechatLoading] = useState(false);
|
const [wechatLoading, setWechatLoading] = useState(false);
|
||||||
const [githubLoading, setGithubLoading] = useState(false);
|
const [githubLoading, setGithubLoading] = useState(false);
|
||||||
|
const [discordLoading, setDiscordLoading] = useState(false);
|
||||||
const [oidcLoading, setOidcLoading] = useState(false);
|
const [oidcLoading, setOidcLoading] = useState(false);
|
||||||
const [linuxdoLoading, setLinuxdoLoading] = useState(false);
|
const [linuxdoLoading, setLinuxdoLoading] = useState(false);
|
||||||
const [emailLoginLoading, setEmailLoginLoading] = useState(false);
|
const [emailLoginLoading, setEmailLoginLoading] = useState(false);
|
||||||
@@ -298,6 +301,21 @@ const LoginForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 包装的Discord登录点击处理
|
||||||
|
const handleDiscordClick = () => {
|
||||||
|
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
|
||||||
|
showInfo(t('请先阅读并同意用户协议和隐私政策'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDiscordLoading(true);
|
||||||
|
try {
|
||||||
|
onDiscordOAuthClicked(status.discord_client_id);
|
||||||
|
} finally {
|
||||||
|
// 由于重定向,这里不会执行到,但为了完整性添加
|
||||||
|
setTimeout(() => setDiscordLoading(false), 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 包装的OIDC登录点击处理
|
// 包装的OIDC登录点击处理
|
||||||
const handleOIDCClick = () => {
|
const handleOIDCClick = () => {
|
||||||
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
|
if ((hasUserAgreement || hasPrivacyPolicy) && !agreedToTerms) {
|
||||||
@@ -472,6 +490,19 @@ const LoginForm = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{status.discord_oauth && (
|
||||||
|
<Button
|
||||||
|
theme='outline'
|
||||||
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
|
type='tertiary'
|
||||||
|
icon={<SiDiscord style={{ color: '#5865F2', width: '20px', height: '20px' }} />}
|
||||||
|
onClick={handleDiscordClick}
|
||||||
|
loading={discordLoading}
|
||||||
|
>
|
||||||
|
<span className='ml-3'>{t('使用 Discord 继续')}</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{status.oidc_enabled && (
|
{status.oidc_enabled && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
@@ -714,6 +745,7 @@ const LoginForm = () => {
|
|||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{(status.github_oauth ||
|
{(status.github_oauth ||
|
||||||
|
status.discord_oauth ||
|
||||||
status.oidc_enabled ||
|
status.oidc_enabled ||
|
||||||
status.wechat_login ||
|
status.wechat_login ||
|
||||||
status.linuxdo_oauth ||
|
status.linuxdo_oauth ||
|
||||||
@@ -849,6 +881,7 @@ const LoginForm = () => {
|
|||||||
{showEmailLogin ||
|
{showEmailLogin ||
|
||||||
!(
|
!(
|
||||||
status.github_oauth ||
|
status.github_oauth ||
|
||||||
|
status.discord_oauth ||
|
||||||
status.oidc_enabled ||
|
status.oidc_enabled ||
|
||||||
status.wechat_login ||
|
status.wechat_login ||
|
||||||
status.linuxdo_oauth ||
|
status.linuxdo_oauth ||
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
updateAPI,
|
updateAPI,
|
||||||
getSystemName,
|
getSystemName,
|
||||||
setUserData,
|
setUserData,
|
||||||
|
onDiscordOAuthClicked,
|
||||||
} from '../../helpers';
|
} from '../../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import { Button, Card, Checkbox, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
|
import { Button, Card, Checkbox, Divider, Form, Icon, Modal } from '@douyinfe/semi-ui';
|
||||||
@@ -51,6 +52,7 @@ import WeChatIcon from '../common/logo/WeChatIcon';
|
|||||||
import TelegramLoginButton from 'react-telegram-login/src';
|
import TelegramLoginButton from 'react-telegram-login/src';
|
||||||
import { UserContext } from '../../context/User';
|
import { UserContext } from '../../context/User';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { SiDiscord } from 'react-icons/si';
|
||||||
|
|
||||||
const RegisterForm = () => {
|
const RegisterForm = () => {
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
@@ -72,6 +74,7 @@ const RegisterForm = () => {
|
|||||||
const [showEmailRegister, setShowEmailRegister] = useState(false);
|
const [showEmailRegister, setShowEmailRegister] = useState(false);
|
||||||
const [wechatLoading, setWechatLoading] = useState(false);
|
const [wechatLoading, setWechatLoading] = useState(false);
|
||||||
const [githubLoading, setGithubLoading] = useState(false);
|
const [githubLoading, setGithubLoading] = useState(false);
|
||||||
|
const [discordLoading, setDiscordLoading] = useState(false);
|
||||||
const [oidcLoading, setOidcLoading] = useState(false);
|
const [oidcLoading, setOidcLoading] = useState(false);
|
||||||
const [linuxdoLoading, setLinuxdoLoading] = useState(false);
|
const [linuxdoLoading, setLinuxdoLoading] = useState(false);
|
||||||
const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
|
const [emailRegisterLoading, setEmailRegisterLoading] = useState(false);
|
||||||
@@ -264,6 +267,15 @@ const RegisterForm = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDiscordClick = () => {
|
||||||
|
setDiscordLoading(true);
|
||||||
|
try {
|
||||||
|
onDiscordOAuthClicked(status.discord_client_id);
|
||||||
|
} finally {
|
||||||
|
setTimeout(() => setDiscordLoading(false), 3000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleOIDCClick = () => {
|
const handleOIDCClick = () => {
|
||||||
setOidcLoading(true);
|
setOidcLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -377,6 +389,19 @@ const RegisterForm = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{status.discord_oauth && (
|
||||||
|
<Button
|
||||||
|
theme='outline'
|
||||||
|
className='w-full h-12 flex items-center justify-center !rounded-full border border-gray-200 hover:bg-gray-50 transition-colors'
|
||||||
|
type='tertiary'
|
||||||
|
icon={<SiDiscord style={{ color: '#5865F2', width: '20px', height: '20px' }} />}
|
||||||
|
onClick={handleDiscordClick}
|
||||||
|
loading={discordLoading}
|
||||||
|
>
|
||||||
|
<span className='ml-3'>{t('使用 Discord 继续')}</span>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{status.oidc_enabled && (
|
{status.oidc_enabled && (
|
||||||
<Button
|
<Button
|
||||||
theme='outline'
|
theme='outline'
|
||||||
@@ -591,6 +616,7 @@ const RegisterForm = () => {
|
|||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
{(status.github_oauth ||
|
{(status.github_oauth ||
|
||||||
|
status.discord_oauth ||
|
||||||
status.oidc_enabled ||
|
status.oidc_enabled ||
|
||||||
status.wechat_login ||
|
status.wechat_login ||
|
||||||
status.linuxdo_oauth ||
|
status.linuxdo_oauth ||
|
||||||
@@ -686,6 +712,7 @@ const RegisterForm = () => {
|
|||||||
{showEmailRegister ||
|
{showEmailRegister ||
|
||||||
!(
|
!(
|
||||||
status.github_oauth ||
|
status.github_oauth ||
|
||||||
|
status.discord_oauth ||
|
||||||
status.oidc_enabled ||
|
status.oidc_enabled ||
|
||||||
status.wechat_login ||
|
status.wechat_login ||
|
||||||
status.linuxdo_oauth ||
|
status.linuxdo_oauth ||
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ const SystemSetting = () => {
|
|||||||
GitHubOAuthEnabled: '',
|
GitHubOAuthEnabled: '',
|
||||||
GitHubClientId: '',
|
GitHubClientId: '',
|
||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
|
DiscordOAuthEnabled: '',
|
||||||
|
DiscordClientId: '',
|
||||||
|
DiscordClientSecret: '',
|
||||||
'oidc.enabled': '',
|
'oidc.enabled': '',
|
||||||
'oidc.client_id': '',
|
'oidc.client_id': '',
|
||||||
'oidc.client_secret': '',
|
'oidc.client_secret': '',
|
||||||
@@ -179,6 +182,7 @@ const SystemSetting = () => {
|
|||||||
case 'EmailAliasRestrictionEnabled':
|
case 'EmailAliasRestrictionEnabled':
|
||||||
case 'SMTPSSLEnabled':
|
case 'SMTPSSLEnabled':
|
||||||
case 'LinuxDOOAuthEnabled':
|
case 'LinuxDOOAuthEnabled':
|
||||||
|
case 'DiscordOAuthEnabled':
|
||||||
case 'oidc.enabled':
|
case 'oidc.enabled':
|
||||||
case 'passkey.enabled':
|
case 'passkey.enabled':
|
||||||
case 'passkey.allow_insecure_origin':
|
case 'passkey.allow_insecure_origin':
|
||||||
@@ -473,6 +477,27 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitDiscordOAuth = async () => {
|
||||||
|
const options = [];
|
||||||
|
|
||||||
|
if (originInputs['DiscordClientId'] !== inputs.DiscordClientId) {
|
||||||
|
options.push({ key: 'DiscordClientId', value: inputs.DiscordClientId });
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['DiscordClientSecret'] !== inputs.DiscordClientSecret &&
|
||||||
|
inputs.DiscordClientSecret !== ''
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
key: 'DiscordClientSecret',
|
||||||
|
value: inputs.DiscordClientSecret,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.length > 0) {
|
||||||
|
await updateOptions(options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitOIDCSettings = async () => {
|
const submitOIDCSettings = async () => {
|
||||||
if (inputs['oidc.well_known'] && inputs['oidc.well_known'] !== '') {
|
if (inputs['oidc.well_known'] && inputs['oidc.well_known'] !== '') {
|
||||||
if (
|
if (
|
||||||
@@ -1014,6 +1039,15 @@ const SystemSetting = () => {
|
|||||||
>
|
>
|
||||||
{t('允许通过 GitHub 账户登录 & 注册')}
|
{t('允许通过 GitHub 账户登录 & 注册')}
|
||||||
</Form.Checkbox>
|
</Form.Checkbox>
|
||||||
|
<Form.Checkbox
|
||||||
|
field='DiscordOAuthEnabled'
|
||||||
|
noLabel
|
||||||
|
onChange={(e) =>
|
||||||
|
handleCheckboxChange('DiscordOAuthEnabled', e)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t('允许通过 Discord 账户登录 & 注册')}
|
||||||
|
</Form.Checkbox>
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
field='LinuxDOOAuthEnabled'
|
field='LinuxDOOAuthEnabled'
|
||||||
noLabel
|
noLabel
|
||||||
@@ -1410,6 +1444,37 @@ const SystemSetting = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Form.Section>
|
</Form.Section>
|
||||||
</Card>
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<Form.Section text={t('配置 Discord OAuth')}>
|
||||||
|
<Text>{t('用以支持通过 Discord 进行登录注册')}</Text>
|
||||||
|
<Banner
|
||||||
|
type='info'
|
||||||
|
description={`${t('Homepage URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')},${t('Authorization callback URL 填')} ${inputs.ServerAddress ? inputs.ServerAddress : t('网站地址')}/oauth/discord`}
|
||||||
|
style={{ marginBottom: 20, marginTop: 16 }}
|
||||||
|
/>
|
||||||
|
<Row
|
||||||
|
gutter={{ xs: 8, sm: 16, md: 24, lg: 24, xl: 24, xxl: 24 }}
|
||||||
|
>
|
||||||
|
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||||
|
<Form.Input
|
||||||
|
field='DiscordClientId'
|
||||||
|
label={t('Discord Client ID')}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={24} md={12} lg={12} xl={12}>
|
||||||
|
<Form.Input
|
||||||
|
field='DiscordClientSecret'
|
||||||
|
label={t('Discord Client Secret')}
|
||||||
|
type='password'
|
||||||
|
placeholder={t('敏感信息不会发送到前端显示')}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Button onClick={submitDiscordOAuth}>
|
||||||
|
{t('保存 Discord OAuth 设置')}
|
||||||
|
</Button>
|
||||||
|
</Form.Section>
|
||||||
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<Form.Section text={t('配置 Linux DO OAuth')}>
|
<Form.Section text={t('配置 Linux DO OAuth')}>
|
||||||
<Text>
|
<Text>
|
||||||
|
|||||||
@@ -38,13 +38,14 @@ import {
|
|||||||
IconLock,
|
IconLock,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
} from '@douyinfe/semi-icons';
|
} from '@douyinfe/semi-icons';
|
||||||
import { SiTelegram, SiWechat, SiLinux } from 'react-icons/si';
|
import { SiTelegram, SiWechat, SiLinux, SiDiscord } from 'react-icons/si';
|
||||||
import { UserPlus, ShieldCheck } from 'lucide-react';
|
import { UserPlus, ShieldCheck } from 'lucide-react';
|
||||||
import TelegramLoginButton from 'react-telegram-login';
|
import TelegramLoginButton from 'react-telegram-login';
|
||||||
import {
|
import {
|
||||||
onGitHubOAuthClicked,
|
onGitHubOAuthClicked,
|
||||||
onOIDCClicked,
|
onOIDCClicked,
|
||||||
onLinuxDOOAuthClicked,
|
onLinuxDOOAuthClicked,
|
||||||
|
onDiscordOAuthClicked,
|
||||||
} from '../../../../helpers';
|
} from '../../../../helpers';
|
||||||
import TwoFASetting from '../components/TwoFASetting';
|
import TwoFASetting from '../components/TwoFASetting';
|
||||||
|
|
||||||
@@ -247,6 +248,47 @@ const AccountManagement = ({
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Discord绑定 */}
|
||||||
|
<Card className='!rounded-xl'>
|
||||||
|
<div className='flex items-center justify-between gap-3'>
|
||||||
|
<div className='flex items-center flex-1 min-w-0'>
|
||||||
|
<div className='w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-700 flex items-center justify-center mr-3 flex-shrink-0'>
|
||||||
|
<SiDiscord
|
||||||
|
size={20}
|
||||||
|
className='text-slate-600 dark:text-slate-300'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='flex-1 min-w-0'>
|
||||||
|
<div className='font-medium text-gray-900'>
|
||||||
|
{t('Discord')}
|
||||||
|
</div>
|
||||||
|
<div className='text-sm text-gray-500 truncate'>
|
||||||
|
{renderAccountInfo(
|
||||||
|
userState.user?.discord_id,
|
||||||
|
t('Discord ID'),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex-shrink-0'>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
theme='outline'
|
||||||
|
size='small'
|
||||||
|
onClick={() =>
|
||||||
|
onDiscordOAuthClicked(status.discord_client_id)
|
||||||
|
}
|
||||||
|
disabled={
|
||||||
|
isBound(userState.user?.discord_id) ||
|
||||||
|
!status.discord_oauth
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{status.discord_oauth ? t('绑定') : t('未启用')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* OIDC绑定 */}
|
{/* OIDC绑定 */}
|
||||||
<Card className='!rounded-xl'>
|
<Card className='!rounded-xl'>
|
||||||
<div className='flex items-center justify-between gap-3'>
|
<div className='flex items-center justify-between gap-3'>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ const EditUserModal = (props) => {
|
|||||||
password: '',
|
password: '',
|
||||||
github_id: '',
|
github_id: '',
|
||||||
oidc_id: '',
|
oidc_id: '',
|
||||||
|
discord_id: '',
|
||||||
wechat_id: '',
|
wechat_id: '',
|
||||||
telegram_id: '',
|
telegram_id: '',
|
||||||
email: '',
|
email: '',
|
||||||
@@ -332,6 +333,7 @@ const EditUserModal = (props) => {
|
|||||||
<Row gutter={12}>
|
<Row gutter={12}>
|
||||||
{[
|
{[
|
||||||
'github_id',
|
'github_id',
|
||||||
|
'discord_id',
|
||||||
'oidc_id',
|
'oidc_id',
|
||||||
'wechat_id',
|
'wechat_id',
|
||||||
'email',
|
'email',
|
||||||
|
|||||||
@@ -231,6 +231,17 @@ export async function getOAuthState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function onDiscordOAuthClicked(client_id) {
|
||||||
|
const state = await getOAuthState();
|
||||||
|
if (!state) return;
|
||||||
|
const redirect_uri = `${window.location.origin}/oauth/discord`;
|
||||||
|
const response_type = 'code';
|
||||||
|
const scope = 'identify+openid';
|
||||||
|
window.open(
|
||||||
|
`https://discord.com/oauth2/authorize?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
|
export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
|
||||||
const state = await getOAuthState();
|
const state = await getOAuthState();
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|||||||
@@ -257,6 +257,7 @@
|
|||||||
"余额充值管理": "余额充值管理",
|
"余额充值管理": "余额充值管理",
|
||||||
"你似乎并没有修改什么": "你似乎并没有修改什么",
|
"你似乎并没有修改什么": "你似乎并没有修改什么",
|
||||||
"使用 GitHub 继续": "使用 GitHub 继续",
|
"使用 GitHub 继续": "使用 GitHub 继续",
|
||||||
|
"使用 Discord 继续": "使用 Discord 继续",
|
||||||
"使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}",
|
"使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}": "使用 JSON 对象格式,格式为:{\"组名\": [最多请求次数, 最多请求完成次数]}",
|
||||||
"使用 LinuxDO 继续": "使用 LinuxDO 继续",
|
"使用 LinuxDO 继续": "使用 LinuxDO 继续",
|
||||||
"使用 OIDC 继续": "使用 OIDC 继续",
|
"使用 OIDC 继续": "使用 OIDC 继续",
|
||||||
|
|||||||
Reference in New Issue
Block a user