diff --git a/common/constants.go b/common/constants.go
index a97fda0e..f29153f2 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -38,6 +38,7 @@ var PasswordLoginEnabled = true
var PasswordRegisterEnabled = true
var EmailVerificationEnabled = false
var GitHubOAuthEnabled = false
+var DiscordOAuthEnabled = false
var WeChatAuthEnabled = false
var TurnstileCheckEnabled = false
var RegisterEnabled = true
@@ -53,6 +54,9 @@ var SMTPToken = ""
var GitHubClientId = ""
var GitHubClientSecret = ""
+var DiscordClientId = ""
+var DiscordClientSecret = ""
+
var WeChatServerAddress = ""
var WeChatServerToken = ""
var WeChatAccountQRCodeImageURL = ""
diff --git a/controller/discord.go b/controller/discord.go
new file mode 100644
index 00000000..f33ae60a
--- /dev/null
+++ b/controller/discord.go
@@ -0,0 +1,195 @@
+package controller
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+ "one-api/common"
+ "one-api/model"
+ "strconv"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
+
+ disgoauth "github.com/realTristan/disgoauth"
+)
+
+type DiscordOAuthResponse struct {
+ AccessToken string `json:"access_token"`
+ Scope string `json:"scope"`
+ TokenType string `json:"token_type"`
+}
+
+type DiscordUser struct {
+ Id string `json:"id"`
+ Username string `json:"username"`
+}
+
+func getDiscordUserInfoByCode(codeFromURLParamaters string, host string) (*DiscordUser, error) {
+ if codeFromURLParamaters == "" {
+ return nil, errors.New("Invalid parameter")
+ }
+
+ // Establish a new discord client
+ var dc *disgoauth.Client = disgoauth.Init(&disgoauth.Client{
+ ClientID: common.DiscordClientId,
+ ClientSecret: common.DiscordClientSecret,
+ RedirectURI: fmt.Sprintf("https://%s/oauth/discord", host),
+ Scopes: []string{disgoauth.ScopeIdentify, disgoauth.ScopeEmail},
+ })
+
+ accessToken, _ := dc.GetOnlyAccessToken(codeFromURLParamaters)
+
+ // Get the authorized user's data using the above accessToken
+ userData, _ := disgoauth.GetUserData(accessToken)
+
+ // Create a new DiscordUser
+ var discordUser DiscordUser
+
+ // Decode the userData map[string]interface{} into the discordUser
+ // Convert the map to JSON
+ jsonData, _ := json.Marshal(userData)
+
+ // Convert the JSON to a struct
+ err := json.Unmarshal(jsonData, &discordUser)
+
+ if err != nil {
+ return nil, err
+ }
+
+ if discordUser.Username == "" {
+ return nil, errors.New("Invalid return value, user field is empty, please try again later!")
+ }
+
+ 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")
+ host := c.Request.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")
+ discordUser, err := getDiscordUserInfoByCode(code, c.Request.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
+}
diff --git a/controller/github.go b/controller/github.go
index e1c64130..8ed84314 100644
--- a/controller/github.go
+++ b/controller/github.go
@@ -5,13 +5,14 @@ import (
"encoding/json"
"errors"
"fmt"
- "github.com/gin-contrib/sessions"
- "github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strconv"
"time"
+
+ "github.com/gin-contrib/sessions"
+ "github.com/gin-gonic/gin"
)
type GitHubOAuthResponse struct {
diff --git a/controller/misc.go b/controller/misc.go
index 755ccbd4..248024d4 100644
--- a/controller/misc.go
+++ b/controller/misc.go
@@ -3,10 +3,11 @@ package controller
import (
"encoding/json"
"fmt"
- "github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
+
+ "github.com/gin-gonic/gin"
)
func GetStatus(c *gin.Context) {
@@ -19,6 +20,8 @@ func GetStatus(c *gin.Context) {
"email_verification": common.EmailVerificationEnabled,
"github_oauth": common.GitHubOAuthEnabled,
"github_client_id": common.GitHubClientId,
+ "discord_oauth": common.DiscordOAuthEnabled,
+ "discord_client_id": common.DiscordClientId,
"system_name": common.SystemName,
"logo": common.Logo,
"footer_html": common.Footer,
diff --git a/controller/option.go b/controller/option.go
index abf0d5be..0d8baa8a 100644
--- a/controller/option.go
+++ b/controller/option.go
@@ -2,11 +2,12 @@ package controller
import (
"encoding/json"
- "github.com/gin-gonic/gin"
"net/http"
"one-api/common"
"one-api/model"
"strings"
+
+ "github.com/gin-gonic/gin"
)
func GetOptions(c *gin.Context) {
@@ -41,6 +42,14 @@ func UpdateOption(c *gin.Context) {
return
}
switch option.Key {
+ 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 "GitHubOAuthEnabled":
if option.Value == "true" && common.GitHubClientId == "" {
c.JSON(http.StatusOK, gin.H{
diff --git a/go.mod b/go.mod
index ff08fc2d..1c3d25ac 100644
--- a/go.mod
+++ b/go.mod
@@ -47,6 +47,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+ github.com/realTristan/disgoauth v1.0.2
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // indirect
golang.org/x/arch v0.4.0 // indirect
diff --git a/go.sum b/go.sum
index b9abb579..d8410454 100644
--- a/go.sum
+++ b/go.sum
@@ -65,8 +65,11 @@ github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
@@ -134,6 +137,10 @@ github.com/pkoukk/tiktoken-go v0.1.4 h1:bniMzWdUvNO6YkRbASo2x5qJf2LAG/TIJojqz+Ig
github.com/pkoukk/tiktoken-go v0.1.4/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/ravener/discord-oauth2 v0.0.0-20230514095040-ae65713199b3 h1:x3LgcvujjG+mx8PUMfPmwn3tcu2aA95uCB6ilGGObWk=
+github.com/ravener/discord-oauth2 v0.0.0-20230514095040-ae65713199b3/go.mod h1:P/mZMYLZ87lqRSECEWsOqywGrO1hlZkk9RTwEw35IP4=
+github.com/realTristan/disgoauth v1.0.2 h1:dfto2Kf1gFlZsf8XuwRNoemLgk+hGn/TJpSdtMrEh8E=
+github.com/realTristan/disgoauth v1.0.2/go.mod h1:t72aRaWMq2gknUZcKONReJlEYFod5sHC86WCJ0X9GxA=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
@@ -163,16 +170,21 @@ golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.4.0 h1:A8WCeEWhLwPBKNbFi5Wv5UTCBx5zzubnXDlMOFAzFMc=
golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
+golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -186,6 +198,7 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -196,7 +209,10 @@ golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
diff --git a/model/option.go b/model/option.go
index 35aeec4c..4bb1425d 100644
--- a/model/option.go
+++ b/model/option.go
@@ -30,6 +30,7 @@ func InitOptionMap() {
common.OptionMap["PasswordRegisterEnabled"] = strconv.FormatBool(common.PasswordRegisterEnabled)
common.OptionMap["EmailVerificationEnabled"] = strconv.FormatBool(common.EmailVerificationEnabled)
common.OptionMap["GitHubOAuthEnabled"] = strconv.FormatBool(common.GitHubOAuthEnabled)
+ common.OptionMap["DiscordOAuthEnabled"] = strconv.FormatBool(common.DiscordOAuthEnabled)
common.OptionMap["WeChatAuthEnabled"] = strconv.FormatBool(common.WeChatAuthEnabled)
common.OptionMap["TurnstileCheckEnabled"] = strconv.FormatBool(common.TurnstileCheckEnabled)
common.OptionMap["RegisterEnabled"] = strconv.FormatBool(common.RegisterEnabled)
@@ -53,6 +54,8 @@ func InitOptionMap() {
common.OptionMap["ServerAddress"] = ""
common.OptionMap["GitHubClientId"] = ""
common.OptionMap["GitHubClientSecret"] = ""
+ common.OptionMap["DiscordClientId"] = ""
+ common.OptionMap["DiscordClientSecret"] = ""
common.OptionMap["WeChatServerAddress"] = ""
common.OptionMap["WeChatServerToken"] = ""
common.OptionMap["WeChatAccountQRCodeImageURL"] = ""
@@ -132,6 +135,8 @@ func updateOptionMap(key string, value string) (err error) {
common.PasswordLoginEnabled = boolValue
case "EmailVerificationEnabled":
common.EmailVerificationEnabled = boolValue
+ case "DiscordOAuthEnabled":
+ common.DiscordOAuthEnabled = boolValue
case "GitHubOAuthEnabled":
common.GitHubOAuthEnabled = boolValue
case "WeChatAuthEnabled":
@@ -170,6 +175,10 @@ func updateOptionMap(key string, value string) (err error) {
common.GitHubClientId = value
case "GitHubClientSecret":
common.GitHubClientSecret = value
+ case "DiscordClientId":
+ common.DiscordClientId = value
+ case "DiscordClientSecret":
+ common.DiscordClientSecret = value
case "Footer":
common.Footer = value
case "SystemName":
diff --git a/model/user.go b/model/user.go
index 7c771840..08327854 100644
--- a/model/user.go
+++ b/model/user.go
@@ -3,9 +3,10 @@ package model
import (
"errors"
"fmt"
- "gorm.io/gorm"
"one-api/common"
"strings"
+
+ "gorm.io/gorm"
)
// 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
Email string `json:"email" gorm:"index" validate:"max=50"`
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"`
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
@@ -169,6 +171,14 @@ func (user *User) FillUserByGitHubId() error {
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 {
if user.WeChatId == "" {
return errors.New("WeChat id 为空!")
@@ -197,6 +207,10 @@ func IsGitHubIdAlreadyTaken(githubId string) bool {
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
}
+func IsDiscordIdAlreadyTaken(discordId string) bool {
+ return DB.Where("discord_id = ?", discordId).Find(&User{}).RowsAffected == 1
+}
+
func IsUsernameAlreadyTaken(username string) bool {
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
}
diff --git a/router/api-router.go b/router/api-router.go
index e89ba4e7..79ed5e4b 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -21,6 +21,7 @@ func SetApiRouter(router *gin.Engine) {
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
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/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
diff --git a/web/src/App.js b/web/src/App.js
index 1c8cba6b..ec91ab73 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -12,6 +12,7 @@ import AddUser from './pages/User/AddUser';
import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
import PasswordResetForm from './components/PasswordResetForm';
import GitHubOAuth from './components/GitHubOAuth';
+import DiscordOAuth from './components/DiscordOAuth';
import PasswordResetConfirm from './components/PasswordResetConfirm';
import { UserContext } from './context/User';
import { StatusContext } from './context/Status';
@@ -230,6 +231,14 @@ function App() {
}
/>
+
+ Discord 身份验证: + {statusState?.status?.discord_oauth === true + ? '已启用' + : '未启用'} +
微信身份验证: {statusState?.status?.wechat_login === true