feat: add Discord OAuth
This commit is contained in:
parent
bc2f48b1f2
commit
4908a9eddc
@ -38,6 +38,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 WeChatAuthEnabled = false
|
var WeChatAuthEnabled = false
|
||||||
var TurnstileCheckEnabled = false
|
var TurnstileCheckEnabled = false
|
||||||
var RegisterEnabled = true
|
var RegisterEnabled = true
|
||||||
@ -53,6 +54,9 @@ var SMTPToken = ""
|
|||||||
var GitHubClientId = ""
|
var GitHubClientId = ""
|
||||||
var GitHubClientSecret = ""
|
var GitHubClientSecret = ""
|
||||||
|
|
||||||
|
var DiscordClientId = ""
|
||||||
|
var DiscordClientSecret = ""
|
||||||
|
|
||||||
var WeChatServerAddress = ""
|
var WeChatServerAddress = ""
|
||||||
var WeChatServerToken = ""
|
var WeChatServerToken = ""
|
||||||
var WeChatAccountQRCodeImageURL = ""
|
var WeChatAccountQRCodeImageURL = ""
|
||||||
|
244
controller/discord.go
Normal file
244
controller/discord.go
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DiscordOAuthResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
Scope string `json:"scope"`
|
||||||
|
TokenType string `json:"token_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DiscordUser struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDiscordUserInfoByCode(codeFromURLParamaters string, host string) (*DiscordUser, error) {
|
||||||
|
if codeFromURLParamaters == "" {
|
||||||
|
return nil, errors.New("无效参数")
|
||||||
|
}
|
||||||
|
|
||||||
|
RequestClient := &http.Client{}
|
||||||
|
|
||||||
|
accessTokenBody := bytes.NewBuffer([]byte(fmt.Sprintf(
|
||||||
|
"client_id=%s&client_secret=%s&grant_type=authorization_code&redirect_uri=%s/oauth/discord&code=%s&scope=identify",
|
||||||
|
common.DiscordClientId, common.DiscordClientSecret, host, codeFromURLParamaters,
|
||||||
|
)))
|
||||||
|
|
||||||
|
req, _ := http.NewRequest("POST",
|
||||||
|
"https://discordapp.com/api/oauth2/token",
|
||||||
|
accessTokenBody,
|
||||||
|
)
|
||||||
|
|
||||||
|
req.Header = http.Header{
|
||||||
|
"Content-Type": []string{"application/x-www-form-urlencoded"},
|
||||||
|
"Accept": []string{"application/json"},
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := RequestClient.Do(req)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 || err != nil {
|
||||||
|
return nil, errors.New("访问令牌无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
var discordOAuthResponse DiscordOAuthResponse
|
||||||
|
|
||||||
|
json.NewDecoder(resp.Body).Decode(&discordOAuthResponse)
|
||||||
|
|
||||||
|
accessToken := fmt.Sprintf("Bearer %s", discordOAuthResponse.AccessToken)
|
||||||
|
|
||||||
|
// Get User Info
|
||||||
|
req, _ = http.NewRequest("GET", "https://discord.com/api/users/@me", nil)
|
||||||
|
|
||||||
|
req.Header = http.Header{
|
||||||
|
"Content-Type": []string{"application/json"},
|
||||||
|
"Authorization": []string{accessToken},
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
resp, err = RequestClient.Do(req)
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 || err != nil {
|
||||||
|
return nil, errors.New("Discord 用户信息无效")
|
||||||
|
}
|
||||||
|
|
||||||
|
var discordUser DiscordUser
|
||||||
|
|
||||||
|
json.NewDecoder(resp.Body).Decode(&discordUser)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if discordUser.Id == "" {
|
||||||
|
return nil, errors.New("返回值无效,用户字段为空,请稍后再试!")
|
||||||
|
}
|
||||||
|
|
||||||
|
if discordUser.Verified == false {
|
||||||
|
return nil, errors.New("Discord 帐户未经验证!")
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return &discordUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DiscordOAuth(c *gin.Context) {
|
||||||
|
session := sessions.Default(c)
|
||||||
|
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")
|
||||||
|
|
||||||
|
// Get protocal whether http or https and host
|
||||||
|
host := c.Request.Host
|
||||||
|
if c.Request.TLS == nil {
|
||||||
|
host = "http://" + host
|
||||||
|
} else {
|
||||||
|
host = "https://" + host
|
||||||
|
}
|
||||||
|
|
||||||
|
discordUser, err := getDiscordUserInfoByCode(code, host)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
DiscordId: discordUser.Id,
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
user.Username = "discord_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
if discordUser.Username != "" {
|
||||||
|
user.DisplayName = discordUser.Username
|
||||||
|
} else {
|
||||||
|
user.DisplayName = "Discord User"
|
||||||
|
}
|
||||||
|
user.Role = common.RoleCommonUser
|
||||||
|
user.Status = common.UserStatusEnabled
|
||||||
|
|
||||||
|
if err := user.Insert(0); 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")
|
||||||
|
|
||||||
|
// Get protocal whether http or https and host
|
||||||
|
host := c.Request.Host
|
||||||
|
if c.Request.TLS == nil {
|
||||||
|
host = "http://" + host
|
||||||
|
} else {
|
||||||
|
host = "https://" + host
|
||||||
|
}
|
||||||
|
|
||||||
|
discordUser, err := getDiscordUserInfoByCode(code, host)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
DiscordId: discordUser.Id,
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user.DiscordId = discordUser.Id
|
||||||
|
err = user.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "bind",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
@ -3,10 +3,11 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetStatus(c *gin.Context) {
|
func GetStatus(c *gin.Context) {
|
||||||
@ -19,6 +20,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,
|
||||||
"system_name": common.SystemName,
|
"system_name": common.SystemName,
|
||||||
"logo": common.Logo,
|
"logo": common.Logo,
|
||||||
"footer_html": common.Footer,
|
"footer_html": common.Footer,
|
||||||
|
@ -2,11 +2,12 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetOptions(c *gin.Context) {
|
func GetOptions(c *gin.Context) {
|
||||||
@ -49,6 +50,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 "WeChatAuthEnabled":
|
case "WeChatAuthEnabled":
|
||||||
if option.Value == "true" && common.WeChatServerAddress == "" {
|
if option.Value == "true" && common.WeChatServerAddress == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
@ -30,6 +30,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["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
|
||||||
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
|
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
|
||||||
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
|
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
|
||||||
@ -53,6 +54,8 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["ServerAddress"] = ""
|
common.OptionMap["ServerAddress"] = ""
|
||||||
common.OptionMap["GitHubClientId"] = ""
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
common.OptionMap["GitHubClientSecret"] = ""
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
|
common.OptionMap["DiscordClientId"] = ""
|
||||||
|
common.OptionMap["DiscordClientSecret"] = ""
|
||||||
common.OptionMap["WeChatServerAddress"] = ""
|
common.OptionMap["WeChatServerAddress"] = ""
|
||||||
common.OptionMap["WeChatServerToken"] = ""
|
common.OptionMap["WeChatServerToken"] = ""
|
||||||
common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
|
||||||
@ -135,6 +138,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 "WeChatAuthEnabled":
|
case "WeChatAuthEnabled":
|
||||||
common.WeChatAuthEnabled = boolValue
|
common.WeChatAuthEnabled = boolValue
|
||||||
case "TurnstileCheckEnabled":
|
case "TurnstileCheckEnabled":
|
||||||
@ -171,6 +176,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 "Footer":
|
case "Footer":
|
||||||
common.Footer = value
|
common.Footer = value
|
||||||
case "SystemName":
|
case "SystemName":
|
||||||
|
@ -3,9 +3,10 @@ package model
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gorm.io/gorm"
|
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// User if you add sensitive fields, don't forget to clean them in setupLogin function.
|
// User if you add sensitive fields, don't forget to clean them in setupLogin function.
|
||||||
@ -19,6 +20,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"`
|
||||||
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
||||||
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
|
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
|
||||||
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
||||||
@ -169,6 +171,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) FillUserByWeChatId() error {
|
func (user *User) FillUserByWeChatId() error {
|
||||||
if user.WeChatId == "" {
|
if user.WeChatId == "" {
|
||||||
return errors.New("WeChat id 为空!")
|
return errors.New("WeChat id 为空!")
|
||||||
@ -193,6 +203,10 @@ func IsWeChatIdAlreadyTaken(wechatId string) bool {
|
|||||||
return DB.Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
|
return DB.Where("wechat_id = ?", wechatId).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsDiscordIdAlreadyTaken(discordId string) bool {
|
||||||
|
return DB.Where("discord_id = ?", discordId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
func IsGitHubIdAlreadyTaken(githubId string) bool {
|
func IsGitHubIdAlreadyTaken(githubId string) bool {
|
||||||
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,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/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
||||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
||||||
|
@ -12,6 +12,7 @@ import AddUser from './pages/User/AddUser';
|
|||||||
import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
|
import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
|
||||||
import PasswordResetForm from './components/PasswordResetForm';
|
import PasswordResetForm from './components/PasswordResetForm';
|
||||||
import GitHubOAuth from './components/GitHubOAuth';
|
import GitHubOAuth from './components/GitHubOAuth';
|
||||||
|
import DiscordOAuth from './components/DiscordOAuth';
|
||||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||||
import { UserContext } from './context/User';
|
import { UserContext } from './context/User';
|
||||||
import { StatusContext } from './context/Status';
|
import { StatusContext } from './context/Status';
|
||||||
@ -239,6 +240,14 @@ function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/oauth/discord'
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<DiscordOAuth />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/setting'
|
path='/setting'
|
||||||
element={
|
element={
|
||||||
|
57
web/src/components/DiscordOAuth.js
Normal file
57
web/src/components/DiscordOAuth.js
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
|
||||||
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import { API, showError, showSuccess } from '../helpers';
|
||||||
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
|
const DiscordOAuth = () => {
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
|
const [prompt, setPrompt] = useState('处理中...');
|
||||||
|
const [processing, setProcessing] = useState(true);
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
const sendCode = async (code, count) => {
|
||||||
|
const res = await API.get(`/api/oauth/discord?code=${code}`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (message === 'bind') {
|
||||||
|
showSuccess('绑定成功!');
|
||||||
|
navigate('/setting');
|
||||||
|
} else {
|
||||||
|
userDispatch({ type: 'login', payload: data });
|
||||||
|
localStorage.setItem('user', JSON.stringify(data));
|
||||||
|
showSuccess('登录成功!');
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
if (count === 0) {
|
||||||
|
setPrompt(`操作失败,重定向至登录界面中...`);
|
||||||
|
navigate('/setting'); // in case this is failed to bind GitHub
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, count * 2000));
|
||||||
|
await sendCode(code, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let code = searchParams.get('code');
|
||||||
|
sendCode(code, 0).then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment style={{ minHeight: '300px' }}>
|
||||||
|
<Dimmer active inverted>
|
||||||
|
<Loader size='large'>{prompt}</Loader>
|
||||||
|
</Dimmer>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DiscordOAuth;
|
@ -37,6 +37,12 @@ const LoginForm = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onDiscordOAuthClicked = () => {
|
||||||
|
window.open(
|
||||||
|
`https://discord.com/oauth2/authorize?response_type=code&client_id=${status.discord_client_id}&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const onWeChatLoginClicked = () => {
|
const onWeChatLoginClicked = () => {
|
||||||
setShowWeChatLoginModal(true);
|
setShowWeChatLoginModal(true);
|
||||||
};
|
};
|
||||||
@ -123,28 +129,32 @@ const LoginForm = () => {
|
|||||||
点击注册
|
点击注册
|
||||||
</Link>
|
</Link>
|
||||||
</Message>
|
</Message>
|
||||||
{status.github_oauth || status.wechat_login ? (
|
{status.github_oauth || status.wechat_login || status.discord_oauth ? (
|
||||||
<>
|
<>
|
||||||
<Divider horizontal>Or</Divider>
|
<Divider horizontal>Or</Divider>
|
||||||
{status.github_oauth ? (
|
{status.discord_oauth && (
|
||||||
|
<Button
|
||||||
|
circular
|
||||||
|
color='blue'
|
||||||
|
icon='discord'
|
||||||
|
onClick={onDiscordOAuthClicked}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{status.github_oauth && (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
color='black'
|
color='black'
|
||||||
icon='github'
|
icon='github'
|
||||||
onClick={onGitHubOAuthClicked}
|
onClick={onGitHubOAuthClicked}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
)}
|
||||||
{status.wechat_login ? (
|
{status.wechat_login && (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
color='green'
|
color='green'
|
||||||
icon='wechat'
|
icon='wechat'
|
||||||
onClick={onWeChatLoginClicked}
|
onClick={onWeChatLoginClicked}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
<></>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
@ -118,6 +118,12 @@ const PersonalSetting = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openDiscordOAuth = () => {
|
||||||
|
window.open(
|
||||||
|
`https://discord.com/api/oauth2/authorize?client_id=${status.discord_client_id}&response_type=code&redirect_uri=${window.location.origin}/oauth/discord&scope=identify`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const sendVerificationCode = async () => {
|
const sendVerificationCode = async () => {
|
||||||
setDisableButton(true);
|
setDisableButton(true);
|
||||||
if (inputs.email === '') return;
|
if (inputs.email === '') return;
|
||||||
@ -215,6 +221,11 @@ const PersonalSetting = () => {
|
|||||||
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
|
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
status.discord_oauth && (
|
||||||
|
<Button onClick={openDiscordOAuth}>绑定 Discord 账号</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowEmailBindModal(true);
|
setShowEmailBindModal(true);
|
||||||
|
@ -8,8 +8,11 @@ const SystemSetting = () => {
|
|||||||
PasswordRegisterEnabled: '',
|
PasswordRegisterEnabled: '',
|
||||||
EmailVerificationEnabled: '',
|
EmailVerificationEnabled: '',
|
||||||
GitHubOAuthEnabled: '',
|
GitHubOAuthEnabled: '',
|
||||||
|
DiscordOAuthEnabled: '',
|
||||||
GitHubClientId: '',
|
GitHubClientId: '',
|
||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
|
DiscordClientId: '',
|
||||||
|
DiscordClientSecret: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '',
|
SMTPPort: '',
|
||||||
@ -56,6 +59,7 @@ const SystemSetting = () => {
|
|||||||
case 'PasswordRegisterEnabled':
|
case 'PasswordRegisterEnabled':
|
||||||
case 'EmailVerificationEnabled':
|
case 'EmailVerificationEnabled':
|
||||||
case 'GitHubOAuthEnabled':
|
case 'GitHubOAuthEnabled':
|
||||||
|
case 'DiscordOAuthEnabled':
|
||||||
case 'WeChatAuthEnabled':
|
case 'WeChatAuthEnabled':
|
||||||
case 'TurnstileCheckEnabled':
|
case 'TurnstileCheckEnabled':
|
||||||
case 'RegisterEnabled':
|
case 'RegisterEnabled':
|
||||||
@ -84,6 +88,8 @@ const SystemSetting = () => {
|
|||||||
name === 'ServerAddress' ||
|
name === 'ServerAddress' ||
|
||||||
name === 'GitHubClientId' ||
|
name === 'GitHubClientId' ||
|
||||||
name === 'GitHubClientSecret' ||
|
name === 'GitHubClientSecret' ||
|
||||||
|
name === 'DiscordClientId' ||
|
||||||
|
name === 'DiscordClientSecret' ||
|
||||||
name === 'WeChatServerAddress' ||
|
name === 'WeChatServerAddress' ||
|
||||||
name === 'WeChatServerToken' ||
|
name === 'WeChatServerToken' ||
|
||||||
name === 'WeChatAccountQRCodeImageURL' ||
|
name === 'WeChatAccountQRCodeImageURL' ||
|
||||||
@ -161,6 +167,18 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitDiscordOAuth = async () => {
|
||||||
|
if (originInputs['DiscordClientId'] !== inputs.DiscordClientId) {
|
||||||
|
await updateOption('DiscordClientId', inputs.DiscordClientId);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['DiscordClientSecret'] !== inputs.DiscordClientSecret &&
|
||||||
|
inputs.DiscordClientSecret !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('DiscordClientSecret', inputs.DiscordClientSecret);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitTurnstile = async () => {
|
const submitTurnstile = async () => {
|
||||||
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
|
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
|
||||||
await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
|
await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
|
||||||
@ -211,6 +229,12 @@ const SystemSetting = () => {
|
|||||||
name='EmailVerificationEnabled'
|
name='EmailVerificationEnabled'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.DiscordOAuthEnabled === 'true'}
|
||||||
|
label='允许通过 Discord 账户登录和注册'
|
||||||
|
name='DiscordOAuthEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
checked={inputs.GitHubOAuthEnabled === 'true'}
|
checked={inputs.GitHubOAuthEnabled === 'true'}
|
||||||
label='允许通过 GitHub 账户登录 & 注册'
|
label='允许通过 GitHub 账户登录 & 注册'
|
||||||
@ -372,6 +396,44 @@ const SystemSetting = () => {
|
|||||||
保存 WeChat Server 设置
|
保存 WeChat Server 设置
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<Header as='h3'>
|
||||||
|
配置 Discord OAuth 应用程序
|
||||||
|
<Header.Subheader>
|
||||||
|
用以支持通过 Discord 进行登录注册,
|
||||||
|
<a href='https://discord.com/developers/applications' target='_blank'>
|
||||||
|
点击此处
|
||||||
|
</a>
|
||||||
|
管理你的 Discord OAuth App
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Message>
|
||||||
|
Homepage URL 填 <code>{inputs.ServerAddress}</code>
|
||||||
|
,Authorization callback URL 填{' '}
|
||||||
|
<code>{`${inputs.ServerAddress}/oauth/discord`}</code>
|
||||||
|
</Message>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='Discord 客户 ID'
|
||||||
|
name='DiscordClientId'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.DiscordClientId}
|
||||||
|
placeholder='输入您注册的 Discord OAuth APP 的 ID'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Discord 客户秘密'
|
||||||
|
name='DiscordClientSecret'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.DiscordClientSecret}
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitDiscordOAuth}>
|
||||||
|
保存 Discord OAuth 设置
|
||||||
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
<Header as='h3'>
|
<Header as='h3'>
|
||||||
配置 Turnstile
|
配置 Turnstile
|
||||||
<Header.Subheader>
|
<Header.Subheader>
|
||||||
|
@ -97,6 +97,12 @@ const Home = () => {
|
|||||||
? '已启用'
|
? '已启用'
|
||||||
: '未启用'}
|
: '未启用'}
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
Discord 身份验证:
|
||||||
|
{statusState?.status?.discord_oauth === true
|
||||||
|
? '已启用'
|
||||||
|
: '未启用'}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
微信身份验证:
|
微信身份验证:
|
||||||
{statusState?.status?.wechat_login === true
|
{statusState?.status?.wechat_login === true
|
||||||
|
@ -13,13 +13,14 @@ const EditUser = () => {
|
|||||||
display_name: '',
|
display_name: '',
|
||||||
password: '',
|
password: '',
|
||||||
github_id: '',
|
github_id: '',
|
||||||
|
discord_id: '',
|
||||||
wechat_id: '',
|
wechat_id: '',
|
||||||
email: '',
|
email: '',
|
||||||
quota: 0,
|
quota: 0,
|
||||||
group: 'default'
|
group: 'default'
|
||||||
});
|
});
|
||||||
const [groupOptions, setGroupOptions] = useState([]);
|
const [groupOptions, setGroupOptions] = useState([]);
|
||||||
const { username, display_name, password, github_id, wechat_id, email, quota, group } =
|
const { username, display_name, password, github_id, wechat_id, email, quota, discord_id } =
|
||||||
inputs;
|
inputs;
|
||||||
const handleInputChange = (e, { name, value }) => {
|
const handleInputChange = (e, { name, value }) => {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
@ -166,6 +167,16 @@ const EditUser = () => {
|
|||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
|
<Form.Field>
|
||||||
|
<Form.Input
|
||||||
|
label='已绑定的 Discord 账户'
|
||||||
|
name='discord_id'
|
||||||
|
value={discord_id}
|
||||||
|
autoComplete='new-password'
|
||||||
|
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='已绑定的邮箱账户'
|
label='已绑定的邮箱账户'
|
||||||
|
Loading…
Reference in New Issue
Block a user