feat: add oidc support
This commit is contained in:
@@ -43,6 +43,7 @@ var PasswordLoginEnabled = true
|
|||||||
var PasswordRegisterEnabled = true
|
var PasswordRegisterEnabled = true
|
||||||
var EmailVerificationEnabled = false
|
var EmailVerificationEnabled = false
|
||||||
var GitHubOAuthEnabled = false
|
var GitHubOAuthEnabled = false
|
||||||
|
var OIDCEnabled = false
|
||||||
var LinuxDOOAuthEnabled = false
|
var LinuxDOOAuthEnabled = false
|
||||||
var WeChatAuthEnabled = false
|
var WeChatAuthEnabled = false
|
||||||
var TelegramOAuthEnabled = false
|
var TelegramOAuthEnabled = false
|
||||||
@@ -78,6 +79,13 @@ var SMTPToken = ""
|
|||||||
var GitHubClientId = ""
|
var GitHubClientId = ""
|
||||||
var GitHubClientSecret = ""
|
var GitHubClientSecret = ""
|
||||||
|
|
||||||
|
var OIDCClientId = ""
|
||||||
|
var OIDCClientSecret = ""
|
||||||
|
var OIDCWellKnown = ""
|
||||||
|
var OIDCAuthorizationEndpoint = ""
|
||||||
|
var OIDCTokenEndpoint = ""
|
||||||
|
var OIDCUserInfoEndpoint = ""
|
||||||
|
|
||||||
var LinuxDOClientId = ""
|
var LinuxDOClientId = ""
|
||||||
var LinuxDOClientSecret = ""
|
var LinuxDOClientSecret = ""
|
||||||
|
|
||||||
|
|||||||
@@ -34,40 +34,43 @@ func GetStatus(c *gin.Context) {
|
|||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
"data": gin.H{
|
"data": gin.H{
|
||||||
"version": common.Version,
|
"version": common.Version,
|
||||||
"start_time": common.StartTime,
|
"start_time": common.StartTime,
|
||||||
"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,
|
||||||
"linuxdo_oauth": common.LinuxDOOAuthEnabled,
|
"linuxdo_oauth": common.LinuxDOOAuthEnabled,
|
||||||
"linuxdo_client_id": common.LinuxDOClientId,
|
"linuxdo_client_id": common.LinuxDOClientId,
|
||||||
"telegram_oauth": common.TelegramOAuthEnabled,
|
"telegram_oauth": common.TelegramOAuthEnabled,
|
||||||
"telegram_bot_name": common.TelegramBotName,
|
"telegram_bot_name": common.TelegramBotName,
|
||||||
"system_name": common.SystemName,
|
"system_name": common.SystemName,
|
||||||
"logo": common.Logo,
|
"logo": common.Logo,
|
||||||
"footer_html": common.Footer,
|
"footer_html": common.Footer,
|
||||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||||
"wechat_login": common.WeChatAuthEnabled,
|
"wechat_login": common.WeChatAuthEnabled,
|
||||||
"server_address": setting.ServerAddress,
|
"server_address": setting.ServerAddress,
|
||||||
"price": setting.Price,
|
"price": setting.Price,
|
||||||
"min_topup": setting.MinTopUp,
|
"min_topup": setting.MinTopUp,
|
||||||
"turnstile_check": common.TurnstileCheckEnabled,
|
"turnstile_check": common.TurnstileCheckEnabled,
|
||||||
"turnstile_site_key": common.TurnstileSiteKey,
|
"turnstile_site_key": common.TurnstileSiteKey,
|
||||||
"top_up_link": common.TopUpLink,
|
"top_up_link": common.TopUpLink,
|
||||||
"docs_link": operation_setting.GetGeneralSetting().DocsLink,
|
"docs_link": operation_setting.GetGeneralSetting().DocsLink,
|
||||||
"quota_per_unit": common.QuotaPerUnit,
|
"quota_per_unit": common.QuotaPerUnit,
|
||||||
"display_in_currency": common.DisplayInCurrencyEnabled,
|
"display_in_currency": common.DisplayInCurrencyEnabled,
|
||||||
"enable_batch_update": common.BatchUpdateEnabled,
|
"enable_batch_update": common.BatchUpdateEnabled,
|
||||||
"enable_drawing": common.DrawingEnabled,
|
"enable_drawing": common.DrawingEnabled,
|
||||||
"enable_task": common.TaskEnabled,
|
"enable_task": common.TaskEnabled,
|
||||||
"enable_data_export": common.DataExportEnabled,
|
"enable_data_export": common.DataExportEnabled,
|
||||||
"data_export_default_time": common.DataExportDefaultTime,
|
"data_export_default_time": common.DataExportDefaultTime,
|
||||||
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
"default_collapse_sidebar": common.DefaultCollapseSidebar,
|
||||||
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
"enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "",
|
||||||
"mj_notify_enabled": setting.MjNotifyEnabled,
|
"mj_notify_enabled": setting.MjNotifyEnabled,
|
||||||
"chats": setting.Chats,
|
"chats": setting.Chats,
|
||||||
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
"demo_site_enabled": operation_setting.DemoSiteEnabled,
|
||||||
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
|
"self_use_mode_enabled": operation_setting.SelfUseModeEnabled,
|
||||||
|
"oidc": common.OIDCEnabled,
|
||||||
|
"oidc_client_id": common.OIDCClientId,
|
||||||
|
"oidc_authorization_endpoint": common.OIDCAuthorizationEndpoint,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
239
controller/oidc.go
Normal file
239
controller/oidc.go
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"one-api/setting"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OidcResponse 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 OidcUser struct {
|
||||||
|
OpenID string `json:"sub"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
PreferredUsername string `json:"preferred_username"`
|
||||||
|
Picture string `json:"picture"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOidcUserInfoByCode(code string) (*OidcUser, error) {
|
||||||
|
if code == "" {
|
||||||
|
return nil, errors.New("无效的参数")
|
||||||
|
}
|
||||||
|
|
||||||
|
values := url.Values{}
|
||||||
|
values.Set("client_id", common.OIDCClientId)
|
||||||
|
values.Set("client_secret", common.OIDCClientSecret)
|
||||||
|
values.Set("code", code)
|
||||||
|
values.Set("grant_type", "authorization_code")
|
||||||
|
values.Set("redirect_uri", fmt.Sprintf("%s/oauth/oidc", setting.ServerAddress))
|
||||||
|
formData := values.Encode()
|
||||||
|
req, err := http.NewRequest("POST", common.OIDCTokenEndpoint, 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("无法连接至 OIDC 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
var oidcResponse OidcResponse
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&oidcResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if oidcResponse.AccessToken == "" {
|
||||||
|
common.SysError("OIDC 获取 Token 失败,请检查设置!")
|
||||||
|
return nil, errors.New("OIDC 获取 Token 失败,请检查设置!")
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = http.NewRequest("GET", common.OIDCUserInfoEndpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+oidcResponse.AccessToken)
|
||||||
|
res2, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至 OIDC 服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res2.Body.Close()
|
||||||
|
if res2.StatusCode != http.StatusOK {
|
||||||
|
common.SysError("OIDC 获取用户信息失败!请检查设置!")
|
||||||
|
return nil, errors.New("OIDC 获取用户信息失败!请检查设置!")
|
||||||
|
}
|
||||||
|
|
||||||
|
var oidcUser OidcUser
|
||||||
|
err = json.NewDecoder(res2.Body).Decode(&oidcUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if oidcUser.OpenID == "" || oidcUser.Email == "" {
|
||||||
|
common.SysError("OIDC 获取用户信息为空!请检查设置!")
|
||||||
|
return nil, errors.New("OIDC 获取用户信息为空!请检查设置!")
|
||||||
|
}
|
||||||
|
return &oidcUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OidcAuth(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 {
|
||||||
|
OidcBind(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !common.OIDCEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 OIDC 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
oidcUser, err := getOidcUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
OidcId: oidcUser.OpenID,
|
||||||
|
}
|
||||||
|
if model.IsOidcIdAlreadyTaken(user.OidcId) {
|
||||||
|
err := user.FillUserByOidcId()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if common.RegisterEnabled {
|
||||||
|
user.Email = oidcUser.Email
|
||||||
|
if oidcUser.PreferredUsername != "" {
|
||||||
|
user.Username = oidcUser.PreferredUsername
|
||||||
|
} else {
|
||||||
|
user.Username = "oidc_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
}
|
||||||
|
if oidcUser.Name != "" {
|
||||||
|
user.DisplayName = oidcUser.Name
|
||||||
|
} else {
|
||||||
|
user.DisplayName = "OIDC 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 OidcBind(c *gin.Context) {
|
||||||
|
if !common.OIDCEnabled {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "管理员未开启通过 OIDC 登录以及注册",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
oidcUser, err := getOidcUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
OidcId: oidcUser.OpenID,
|
||||||
|
}
|
||||||
|
if model.IsOidcIdAlreadyTaken(user.OidcId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该 OIDC 账户已被绑定",
|
||||||
|
})
|
||||||
|
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.OidcId = oidcUser.OpenID
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -51,6 +51,13 @@ func UpdateOption(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
case "OIDCEnabled":
|
||||||
|
if option.Value == "true" && common.OIDCClientId == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "无法启用 OIDC 登录,请先填入 OIDC Client Id 以及 OIDC Client Secret!",
|
||||||
|
})
|
||||||
|
}
|
||||||
case "LinuxDOOAuthEnabled":
|
case "LinuxDOOAuthEnabled":
|
||||||
if option.Value == "true" && common.LinuxDOClientId == "" {
|
if option.Value == "true" && common.LinuxDOClientId == "" {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
|||||||
10
go.mod
10
go.mod
@@ -28,9 +28,9 @@ require (
|
|||||||
github.com/samber/lo v1.39.0
|
github.com/samber/lo v1.39.0
|
||||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||||
github.com/shopspring/decimal v1.4.0
|
github.com/shopspring/decimal v1.4.0
|
||||||
golang.org/x/crypto v0.27.0
|
golang.org/x/crypto v0.35.0
|
||||||
golang.org/x/image v0.23.0
|
golang.org/x/image v0.23.0
|
||||||
golang.org/x/net v0.28.0
|
golang.org/x/net v0.35.0
|
||||||
gorm.io/driver/mysql v1.4.3
|
gorm.io/driver/mysql v1.4.3
|
||||||
gorm.io/driver/postgres v1.5.2
|
gorm.io/driver/postgres v1.5.2
|
||||||
gorm.io/gorm v1.25.2
|
gorm.io/gorm v1.25.2
|
||||||
@@ -84,9 +84,9 @@ require (
|
|||||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||||
golang.org/x/arch v0.12.0 // indirect
|
golang.org/x/arch v0.12.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
golang.org/x/sys v0.27.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
google.golang.org/protobuf v1.34.2 // indirect
|
google.golang.org/protobuf v1.34.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
modernc.org/libc v1.22.5 // indirect
|
modernc.org/libc v1.22.5 // indirect
|
||||||
|
|||||||
20
go.sum
20
go.sum
@@ -217,18 +217,18 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
|
|||||||
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg=
|
||||||
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
|
||||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8=
|
||||||
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -239,14 +239,14 @@ golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
|||||||
@@ -35,6 +35,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["OIDCEnabled"] = strconv.FormatBool(common.OIDCEnabled)
|
||||||
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)
|
||||||
@@ -77,6 +78,12 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
common.OptionMap["Chats"] = setting.Chats2JsonString()
|
||||||
common.OptionMap["GitHubClientId"] = ""
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
common.OptionMap["GitHubClientSecret"] = ""
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
|
common.OptionMap["OIDCClientId"] = ""
|
||||||
|
common.OptionMap["OIDCClientSecret"] = ""
|
||||||
|
common.OptionMap["OIDCWellKnown"] = ""
|
||||||
|
common.OptionMap["OIDCAuthorizationEndpoint"] = ""
|
||||||
|
common.OptionMap["OIDCTokenEndpoint"] = ""
|
||||||
|
common.OptionMap["OIDCUserInfoEndpoint"] = ""
|
||||||
common.OptionMap["TelegramBotToken"] = ""
|
common.OptionMap["TelegramBotToken"] = ""
|
||||||
common.OptionMap["TelegramBotName"] = ""
|
common.OptionMap["TelegramBotName"] = ""
|
||||||
common.OptionMap["WeChatServerAddress"] = ""
|
common.OptionMap["WeChatServerAddress"] = ""
|
||||||
@@ -200,6 +207,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 "OIDCEnabled":
|
||||||
|
common.OIDCEnabled = boolValue
|
||||||
case "LinuxDOOAuthEnabled":
|
case "LinuxDOOAuthEnabled":
|
||||||
common.LinuxDOOAuthEnabled = boolValue
|
common.LinuxDOOAuthEnabled = boolValue
|
||||||
case "WeChatAuthEnabled":
|
case "WeChatAuthEnabled":
|
||||||
@@ -298,6 +307,18 @@ 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 "OIDCClientId":
|
||||||
|
common.OIDCClientId = value
|
||||||
|
case "OIDCClientSecret":
|
||||||
|
common.OIDCClientSecret = value
|
||||||
|
case "OIDCWellKnown":
|
||||||
|
common.OIDCWellKnown = value
|
||||||
|
case "OIDCAuthorizationEndpoint":
|
||||||
|
common.OIDCAuthorizationEndpoint = value
|
||||||
|
case "OIDCTokenEndpoint":
|
||||||
|
common.OIDCTokenEndpoint = value
|
||||||
|
case "OIDCUserInfoEndpoint":
|
||||||
|
common.OIDCUserInfoEndpoint = value
|
||||||
case "LinuxDOClientId":
|
case "LinuxDOClientId":
|
||||||
common.LinuxDOClientId = value
|
common.LinuxDOClientId = value
|
||||||
case "LinuxDOClientSecret":
|
case "LinuxDOClientSecret":
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bytedance/gopkg/util/gopool"
|
"github.com/bytedance/gopkg/util/gopool"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +23,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"`
|
||||||
|
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"`
|
||||||
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!
|
||||||
@@ -442,6 +442,14 @@ func (user *User) FillUserByGitHubId() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByOidcId() error {
|
||||||
|
if user.OidcId == "" {
|
||||||
|
return errors.New("oidc id 为空!")
|
||||||
|
}
|
||||||
|
DB.Where(User{OidcId: user.OidcId}).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 为空!")
|
||||||
@@ -473,6 +481,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 IsOidcIdAlreadyTaken(oidcId string) bool {
|
||||||
|
return DB.Where("oidc_id = ?", oidcId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
func IsTelegramIdAlreadyTaken(telegramId string) bool {
|
func IsTelegramIdAlreadyTaken(telegramId string) bool {
|
||||||
return DB.Unscoped().Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1
|
return DB.Unscoped().Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,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/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)
|
||||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||||
|
|||||||
@@ -160,6 +160,14 @@ function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/oauth/oidc'
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<OAuth2Callback type='oidc'></OAuth2Callback>
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/oauth/linuxdo'
|
path='/oauth/linuxdo'
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
showSuccess,
|
showSuccess,
|
||||||
updateAPI,
|
updateAPI,
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import { onGitHubOAuthClicked, onLinuxDOOAuthClicked } from './utils';
|
import {onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked} from './utils';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -25,6 +25,7 @@ import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
|||||||
import TelegramLoginButton from 'react-telegram-login';
|
import TelegramLoginButton from 'react-telegram-login';
|
||||||
|
|
||||||
import { IconGithubLogo, IconAlarm } from '@douyinfe/semi-icons';
|
import { IconGithubLogo, IconAlarm } from '@douyinfe/semi-icons';
|
||||||
|
import OIDCIcon from './OIDCIcon.js';
|
||||||
import WeChatIcon from './WeChatIcon';
|
import WeChatIcon from './WeChatIcon';
|
||||||
import { setUserData } from '../helpers/data.js';
|
import { setUserData } from '../helpers/data.js';
|
||||||
import LinuxDoIcon from './LinuxDoIcon.js';
|
import LinuxDoIcon from './LinuxDoIcon.js';
|
||||||
@@ -229,6 +230,7 @@ const LoginForm = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{status.github_oauth ||
|
{status.github_oauth ||
|
||||||
|
status.oidc ||
|
||||||
status.wechat_login ||
|
status.wechat_login ||
|
||||||
status.telegram_oauth ||
|
status.telegram_oauth ||
|
||||||
status.linuxdo_oauth ? (
|
status.linuxdo_oauth ? (
|
||||||
@@ -254,6 +256,17 @@ const LoginForm = () => {
|
|||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
{status.oidc ? (
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
icon={<OIDCIcon />}
|
||||||
|
onClick={() =>
|
||||||
|
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
{status.linuxdo_oauth ? (
|
{status.linuxdo_oauth ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<LinuxDoIcon />}
|
icon={<LinuxDoIcon />}
|
||||||
|
|||||||
22
web/src/components/OIDCIcon.js
Normal file
22
web/src/components/OIDCIcon.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Icon } from '@douyinfe/semi-ui';
|
||||||
|
|
||||||
|
const OIDCIcon = (props) => {
|
||||||
|
function CustomIcon() {
|
||||||
|
return (
|
||||||
|
<svg t="1723135116886" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
p-id="10969" width="1em" height="1em">
|
||||||
|
<path
|
||||||
|
d="M512 960C265 960 64 759 64 512S265 64 512 64s448 201 448 448-201 448-448 448z m0-882.6c-239.7 0-434.6 195-434.6 434.6s195 434.6 434.6 434.6 434.6-195 434.6-434.6S751.7 77.4 512 77.4z"
|
||||||
|
p-id="10970" fill="#2c2c2c" stroke="#2c2c2c" stroke-width="60"></path>
|
||||||
|
<path
|
||||||
|
d="M197.7 512c0-78.3 31.6-98.8 87.2-98.8 56.2 0 87.2 20.5 87.2 98.8s-31 98.8-87.2 98.8c-55.7 0-87.2-20.5-87.2-98.8z m130.4 0c0-46.8-7.8-64.5-43.2-64.5-35.2 0-42.9 17.7-42.9 64.5 0 47.1 7.8 63.7 42.9 63.7 35.4 0 43.2-16.6 43.2-63.7zM409.7 415.9h42.1V608h-42.1V415.9zM653.9 512c0 74.2-37.1 96.1-93.6 96.1h-65.9V415.9h65.9c56.5 0 93.6 16.1 93.6 96.1z m-43.5 0c0-49.3-17.7-60.6-52.3-60.6h-21.6v120.7h21.6c35.4 0 52.3-13.3 52.3-60.1zM686.5 512c0-74.2 36.3-98.8 92.7-98.8 18.3 0 33.2 2.2 44.8 6.4v36.3c-11.9-4.2-26-6.6-42.1-6.6-34.6 0-49.8 15.5-49.8 62.6 0 50.1 15.2 62.6 49.3 62.6 15.8 0 30.2-2.2 44.8-7.5v36c-11.3 4.7-28.5 8-46.8 8-56.1-0.2-92.9-18.7-92.9-99z"
|
||||||
|
p-id="10971" fill="#2c2c2c" stroke="#2c2c2c" stroke-width="20"></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Icon svg={<CustomIcon />} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OIDCIcon;
|
||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import {UserContext} from '../context/User';
|
import {UserContext} from '../context/User';
|
||||||
import {onGitHubOAuthClicked, onLinuxDOOAuthClicked} from './utils';
|
import {onGitHubOAuthClicked, onOIDCClicked, onLinuxDOOAuthClicked} from './utils';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Banner,
|
Banner,
|
||||||
@@ -640,6 +640,36 @@ const PersonalSetting = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style={{marginTop: 10}}>
|
||||||
|
<Typography.Text strong>{t('OIDC')}</Typography.Text>
|
||||||
|
<div
|
||||||
|
style={{display: 'flex', justifyContent: 'space-between'}}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
userState.user && userState.user.oidc_id !== ''
|
||||||
|
? userState.user.oidc_id
|
||||||
|
: t('未绑定')
|
||||||
|
}
|
||||||
|
readonly={true}
|
||||||
|
></Input>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id);
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
(userState.user && userState.user.oidc_id !== '') ||
|
||||||
|
!status.oidc
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{status.oidc ? t('绑定') : t('未启用')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div style={{marginTop: 10}}>
|
<div style={{marginTop: 10}}>
|
||||||
<Typography.Text strong>{t('Telegram')}</Typography.Text>
|
<Typography.Text strong>{t('Telegram')}</Typography.Text>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { Button, Card, Divider, Form, Icon, Layout, Modal } from '@douyinfe/semi
|
|||||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
|
||||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
|
||||||
import { IconGithubLogo } from '@douyinfe/semi-icons';
|
import { IconGithubLogo } from '@douyinfe/semi-icons';
|
||||||
import { onGitHubOAuthClicked, onLinuxDOOAuthClicked } from './utils.js';
|
import {onGitHubOAuthClicked, onLinuxDOOAuthClicked, onOIDCClicked} from './utils.js';
|
||||||
|
import OIDCIcon from "./OIDCIcon.js";
|
||||||
import LinuxDoIcon from './LinuxDoIcon.js';
|
import LinuxDoIcon from './LinuxDoIcon.js';
|
||||||
import WeChatIcon from './WeChatIcon.js';
|
import WeChatIcon from './WeChatIcon.js';
|
||||||
import TelegramLoginButton from 'react-telegram-login/src';
|
import TelegramLoginButton from 'react-telegram-login/src';
|
||||||
@@ -262,6 +263,7 @@ const RegisterForm = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
{status.github_oauth ||
|
{status.github_oauth ||
|
||||||
|
status.oidc ||
|
||||||
status.wechat_login ||
|
status.wechat_login ||
|
||||||
status.telegram_oauth ||
|
status.telegram_oauth ||
|
||||||
status.linuxdo_oauth ? (
|
status.linuxdo_oauth ? (
|
||||||
@@ -287,6 +289,17 @@ const RegisterForm = () => {
|
|||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
{status.oidc ? (
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
icon={<OIDCIcon />}
|
||||||
|
onClick={() =>
|
||||||
|
onOIDCClicked(status.oidc_authorization_endpoint, status.oidc_client_id)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
{status.linuxdo_oauth ? (
|
{status.linuxdo_oauth ? (
|
||||||
<Button
|
<Button
|
||||||
icon={<LinuxDoIcon />}
|
icon={<LinuxDoIcon />}
|
||||||
|
|||||||
@@ -20,6 +20,13 @@ const SystemSetting = () => {
|
|||||||
GitHubOAuthEnabled: '',
|
GitHubOAuthEnabled: '',
|
||||||
GitHubClientId: '',
|
GitHubClientId: '',
|
||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
|
OIDCEnabled: '',
|
||||||
|
OIDCClientId: '',
|
||||||
|
OIDCClientSecret: '',
|
||||||
|
OIDCWellKnown: '',
|
||||||
|
OIDCAuthorizationEndpoint: '',
|
||||||
|
OIDCTokenEndpoint: '',
|
||||||
|
OIDCUserInfoEndpoint: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '',
|
SMTPPort: '',
|
||||||
@@ -106,6 +113,7 @@ const SystemSetting = () => {
|
|||||||
case 'PasswordRegisterEnabled':
|
case 'PasswordRegisterEnabled':
|
||||||
case 'EmailVerificationEnabled':
|
case 'EmailVerificationEnabled':
|
||||||
case 'GitHubOAuthEnabled':
|
case 'GitHubOAuthEnabled':
|
||||||
|
case 'OIDCEnabled':
|
||||||
case 'LinuxDOOAuthEnabled':
|
case 'LinuxDOOAuthEnabled':
|
||||||
case 'WeChatAuthEnabled':
|
case 'WeChatAuthEnabled':
|
||||||
case 'TelegramOAuthEnabled':
|
case 'TelegramOAuthEnabled':
|
||||||
@@ -159,6 +167,12 @@ const SystemSetting = () => {
|
|||||||
name === 'PayAddress' ||
|
name === 'PayAddress' ||
|
||||||
name === 'GitHubClientId' ||
|
name === 'GitHubClientId' ||
|
||||||
name === 'GitHubClientSecret' ||
|
name === 'GitHubClientSecret' ||
|
||||||
|
name === 'OIDCWellKnown' ||
|
||||||
|
name === 'OIDCClientId' ||
|
||||||
|
name === 'OIDCClientSecret' ||
|
||||||
|
name === 'OIDCAuthorizationEndpoint' ||
|
||||||
|
name === 'OIDCTokenEndpoint' ||
|
||||||
|
name === 'OIDCUserInfoEndpoint' ||
|
||||||
name === 'WeChatServerAddress' ||
|
name === 'WeChatServerAddress' ||
|
||||||
name === 'WeChatServerToken' ||
|
name === 'WeChatServerToken' ||
|
||||||
name === 'WeChatAccountQRCodeImageURL' ||
|
name === 'WeChatAccountQRCodeImageURL' ||
|
||||||
@@ -286,6 +300,43 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitOIDCSettings = async () => {
|
||||||
|
if (inputs.OIDCWellKnown !== '') {
|
||||||
|
if (!inputs.OIDCWellKnown.startsWith('http://') && !inputs.OIDCWellKnown.startsWith('https://')) {
|
||||||
|
showError('Well-Known URL 必须以 http:// 或 https:// 开头');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await API.get(inputs.OIDCWellKnown);
|
||||||
|
inputs.OIDCAuthorizationEndpoint = res.data['authorization_endpoint'];
|
||||||
|
inputs.OIDCTokenEndpoint = res.data['token_endpoint'];
|
||||||
|
inputs.OIDCUserInfoEndpoint = res.data['userinfo_endpoint'];
|
||||||
|
showSuccess('获取 OIDC 配置成功!');
|
||||||
|
} catch (err) {
|
||||||
|
showError("获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (originInputs['OIDCWellKnown'] !== inputs.OIDCWellKnown) {
|
||||||
|
await updateOption('OIDCWellKnown', inputs.OIDCWellKnown);
|
||||||
|
}
|
||||||
|
if (originInputs['OIDCClientId'] !== inputs.OIDCClientId) {
|
||||||
|
await updateOption('OIDCClientId', inputs.OIDCClientId);
|
||||||
|
}
|
||||||
|
if (originInputs['OIDCClientSecret'] !== inputs.OIDCClientSecret && inputs.OIDCClientSecret !== '') {
|
||||||
|
await updateOption('OIDCClientSecret', inputs.OIDCClientSecret);
|
||||||
|
}
|
||||||
|
if (originInputs['OIDCAuthorizationEndpoint'] !== inputs.OIDCAuthorizationEndpoint) {
|
||||||
|
await updateOption('OIDCAuthorizationEndpoint', inputs.OIDCAuthorizationEndpoint);
|
||||||
|
}
|
||||||
|
if (originInputs['OIDCTokenEndpoint'] !== inputs.OIDCTokenEndpoint) {
|
||||||
|
await updateOption('OIDCTokenEndpoint', inputs.OIDCTokenEndpoint);
|
||||||
|
}
|
||||||
|
if (originInputs['OIDCUserInfoEndpoint'] !== inputs.OIDCUserInfoEndpoint) {
|
||||||
|
await updateOption('OIDCUserInfoEndpoint', inputs.OIDCUserInfoEndpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const submitTelegramSettings = async () => {
|
const submitTelegramSettings = async () => {
|
||||||
// await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled);
|
// await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled);
|
||||||
await updateOption('TelegramBotToken', inputs.TelegramBotToken);
|
await updateOption('TelegramBotToken', inputs.TelegramBotToken);
|
||||||
@@ -370,7 +421,7 @@ const SystemSetting = () => {
|
|||||||
</Header>
|
</Header>
|
||||||
<Message info>
|
<Message info>
|
||||||
注意:代理功能仅对图片请求和 Webhook 请求生效,不会影响其他 API 请求。如需配置 API 请求代理,请参考
|
注意:代理功能仅对图片请求和 Webhook 请求生效,不会影响其他 API 请求。如需配置 API 请求代理,请参考
|
||||||
<a
|
<a
|
||||||
href='https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md'
|
href='https://github.com/Calcium-Ion/new-api/blob/main/docs/channel/other_setting.md'
|
||||||
target='_blank'
|
target='_blank'
|
||||||
rel='noreferrer'
|
rel='noreferrer'
|
||||||
@@ -518,6 +569,12 @@ const SystemSetting = () => {
|
|||||||
name='GitHubOAuthEnabled'
|
name='GitHubOAuthEnabled'
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
/>
|
/>
|
||||||
|
<Form.Checkbox
|
||||||
|
checked={inputs.OIDCEnabled === 'true'}
|
||||||
|
label='允许通过 OIDC 登录 & 注册'
|
||||||
|
name='OIDCEnabled'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
<Form.Checkbox
|
<Form.Checkbox
|
||||||
checked={inputs.LinuxDOOAuthEnabled === 'true'}
|
checked={inputs.LinuxDOOAuthEnabled === 'true'}
|
||||||
label='允许通过 LinuxDO 账户登录 & 注册'
|
label='允许通过 LinuxDO 账户登录 & 注册'
|
||||||
@@ -864,6 +921,68 @@ const SystemSetting = () => {
|
|||||||
<Form.Button onClick={submitLinuxDOOAuth}>
|
<Form.Button onClick={submitLinuxDOOAuth}>
|
||||||
保存 LinuxDO OAuth 设置
|
保存 LinuxDO OAuth 设置
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3' inverted={isDark}>
|
||||||
|
配置 OIDC
|
||||||
|
<Header.Subheader>
|
||||||
|
用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 IdP
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Message>
|
||||||
|
主页链接填 <code>{ inputs.ServerAddress }</code>,
|
||||||
|
重定向 URL 填 <code>{ `${ inputs.ServerAddress }/oauth/oidc` }</code>
|
||||||
|
</Message>
|
||||||
|
<Message>
|
||||||
|
若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 OIDC Well-Known URL,系统会自动获取 OIDC 配置
|
||||||
|
</Message>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='Client ID'
|
||||||
|
name='OIDCClientId'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.OIDCClientId}
|
||||||
|
placeholder='输入 OIDC 的 Client ID'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Client Secret'
|
||||||
|
name='OIDCClientSecret'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='password'
|
||||||
|
value={inputs.OIDCClientSecret}
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Well-Known URL'
|
||||||
|
name='OIDCWellKnown'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.OIDCWellKnown}
|
||||||
|
placeholder='请输入 OIDC 的 Well-Known URL'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Authorization Endpoint'
|
||||||
|
name='OIDCAuthorizationEndpoint'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.OIDCAuthorizationEndpoint}
|
||||||
|
placeholder='输入 OIDC 的 Authorization Endpoint'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Token Endpoint'
|
||||||
|
name='OIDCTokenEndpoint'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.OIDCTokenEndpoint}
|
||||||
|
placeholder='输入 OIDC 的 Token Endpoint'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='Userinfo Endpoint'
|
||||||
|
name='OIDCUserInfoEndpoint'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
value={inputs.OIDCUserInfoEndpoint}
|
||||||
|
placeholder='输入 OIDC 的 Userinfo Endpoint'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitOIDCSettings}>
|
||||||
|
保存 OIDC 设置
|
||||||
|
</Form.Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -16,6 +16,21 @@ export async function getOAuthState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) {
|
||||||
|
const state = await getOAuthState();
|
||||||
|
if (!state) return;
|
||||||
|
const redirect_uri = `${window.location.origin}/oauth/oidc`;
|
||||||
|
const response_type = "code";
|
||||||
|
const scope = "openid profile email";
|
||||||
|
const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`;
|
||||||
|
if (openInNewTab) {
|
||||||
|
window.open(url);
|
||||||
|
} else
|
||||||
|
{
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function onGitHubOAuthClicked(github_client_id) {
|
export async function onGitHubOAuthClicked(github_client_id) {
|
||||||
const state = await getOAuthState();
|
const state = await getOAuthState();
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
|||||||
@@ -151,6 +151,12 @@ const Home = () => {
|
|||||||
? t('已启用')
|
? t('已启用')
|
||||||
: t('未启用')}
|
: t('未启用')}
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
{t('OIDC 身份验证')}:
|
||||||
|
{statusState?.status?.oidc === true
|
||||||
|
? t('已启用')
|
||||||
|
: t('未启用')}
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{t('微信身份验证')}:
|
{t('微信身份验证')}:
|
||||||
{statusState?.status?.wechat_login === true
|
{statusState?.status?.wechat_login === true
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ const EditUser = (props) => {
|
|||||||
display_name: '',
|
display_name: '',
|
||||||
password: '',
|
password: '',
|
||||||
github_id: '',
|
github_id: '',
|
||||||
|
oidc_id: '',
|
||||||
wechat_id: '',
|
wechat_id: '',
|
||||||
email: '',
|
email: '',
|
||||||
quota: 0,
|
quota: 0,
|
||||||
@@ -37,6 +38,7 @@ const EditUser = (props) => {
|
|||||||
display_name,
|
display_name,
|
||||||
password,
|
password,
|
||||||
github_id,
|
github_id,
|
||||||
|
oidc_id,
|
||||||
wechat_id,
|
wechat_id,
|
||||||
telegram_id,
|
telegram_id,
|
||||||
email,
|
email,
|
||||||
@@ -232,6 +234,15 @@ const EditUser = (props) => {
|
|||||||
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
|
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
|
<div style={{ marginTop: 20 }}>
|
||||||
|
<Typography.Text>{t('`已绑定的 OIDC 账户')}</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
name='oidc_id'
|
||||||
|
value={oidc_id}
|
||||||
|
placeholder={t('此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改')}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
<div style={{ marginTop: 20 }}>
|
<div style={{ marginTop: 20 }}>
|
||||||
<Typography.Text>{t('已绑定的微信账户')}</Typography.Text>
|
<Typography.Text>{t('已绑定的微信账户')}</Typography.Text>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user