feat: support feishu login now
This commit is contained in:
parent
68605800af
commit
4d61b9937b
@ -105,6 +105,7 @@ _✨ 通过标准的 OpenAI API 格式访问所有的大模型,开箱即用
|
|||||||
21. 支持 Cloudflare Turnstile 用户校验。
|
21. 支持 Cloudflare Turnstile 用户校验。
|
||||||
22. 支持用户管理,支持**多种用户登录注册方式**:
|
22. 支持用户管理,支持**多种用户登录注册方式**:
|
||||||
+ 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。
|
+ 邮箱登录注册(支持注册邮箱白名单)以及通过邮箱进行密码重置。
|
||||||
|
+ 支持使用飞书进行授权登录。
|
||||||
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
||||||
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
||||||
23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。
|
23. 支持主题切换,设置环境变量 `THEME` 即可,默认为 `default`,欢迎 PR 更多主题,具体参考[此处](./web/README.md)。
|
||||||
|
@ -66,6 +66,9 @@ var SMTPToken = ""
|
|||||||
var GitHubClientId = ""
|
var GitHubClientId = ""
|
||||||
var GitHubClientSecret = ""
|
var GitHubClientSecret = ""
|
||||||
|
|
||||||
|
var LarkClientId = ""
|
||||||
|
var LarkClientSecret = ""
|
||||||
|
|
||||||
var WeChatServerAddress = ""
|
var WeChatServerAddress = ""
|
||||||
var WeChatServerToken = ""
|
var WeChatServerToken = ""
|
||||||
var WeChatAccountQRCodeImageURL = ""
|
var WeChatAccountQRCodeImageURL = ""
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package controller
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/songquanpeng/one-api/common/config"
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
"github.com/songquanpeng/one-api/common/helper"
|
"github.com/songquanpeng/one-api/common/helper"
|
||||||
"github.com/songquanpeng/one-api/common/logger"
|
"github.com/songquanpeng/one-api/common/logger"
|
||||||
|
"github.com/songquanpeng/one-api/controller"
|
||||||
"github.com/songquanpeng/one-api/model"
|
"github.com/songquanpeng/one-api/model"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -159,7 +160,7 @@ func GitHubOAuth(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setupLogin(&user, c)
|
controller.SetupLogin(&user, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GitHubBind(c *gin.Context) {
|
func GitHubBind(c *gin.Context) {
|
201
controller/auth/lark.go
Normal file
201
controller/auth/lark.go
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/songquanpeng/one-api/common"
|
||||||
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
|
"github.com/songquanpeng/one-api/common/logger"
|
||||||
|
"github.com/songquanpeng/one-api/controller"
|
||||||
|
"github.com/songquanpeng/one-api/model"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LarkOAuthResponse struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LarkUser struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
OpenID string `json:"open_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLarkUserInfoByCode(code string) (*LarkUser, error) {
|
||||||
|
if code == "" {
|
||||||
|
return nil, errors.New("无效的参数")
|
||||||
|
}
|
||||||
|
values := map[string]string{
|
||||||
|
"client_id": config.LarkClientId,
|
||||||
|
"client_secret": config.LarkClientSecret,
|
||||||
|
"code": code,
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"redirect_uri": fmt.Sprintf("%s/oauth/lark", config.ServerAddress),
|
||||||
|
}
|
||||||
|
jsonData, err := json.Marshal(values)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", "https://passport.feishu.cn/suite/passport/oauth/token", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至飞书服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
var oAuthResponse LarkOAuthResponse
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&oAuthResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest("GET", "https://passport.feishu.cn/suite/passport/oauth/userinfo", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", oAuthResponse.AccessToken))
|
||||||
|
res2, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
logger.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至飞书服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
var larkUser LarkUser
|
||||||
|
err = json.NewDecoder(res2.Body).Decode(&larkUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &larkUser, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func LarkOAuth(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 {
|
||||||
|
LarkBind(c)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
code := c.Query("code")
|
||||||
|
larkUser, err := getLarkUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
LarkId: larkUser.OpenID,
|
||||||
|
}
|
||||||
|
if model.IsLarkIdAlreadyTaken(user.LarkId) {
|
||||||
|
err := user.FillUserByLarkId()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if config.RegisterEnabled {
|
||||||
|
user.Username = "lark_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
if larkUser.Name != "" {
|
||||||
|
user.DisplayName = larkUser.Name
|
||||||
|
} else {
|
||||||
|
user.DisplayName = "Lark 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
|
||||||
|
}
|
||||||
|
controller.SetupLogin(&user, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LarkBind(c *gin.Context) {
|
||||||
|
code := c.Query("code")
|
||||||
|
larkUser, err := getLarkUserInfoByCode(code)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{
|
||||||
|
LarkId: larkUser.OpenID,
|
||||||
|
}
|
||||||
|
if model.IsLarkIdAlreadyTaken(user.LarkId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该飞书账户已被绑定",
|
||||||
|
})
|
||||||
|
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.LarkId = larkUser.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
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package controller
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/songquanpeng/one-api/common"
|
"github.com/songquanpeng/one-api/common"
|
||||||
"github.com/songquanpeng/one-api/common/config"
|
"github.com/songquanpeng/one-api/common/config"
|
||||||
|
"github.com/songquanpeng/one-api/controller"
|
||||||
"github.com/songquanpeng/one-api/model"
|
"github.com/songquanpeng/one-api/model"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@ -109,7 +110,7 @@ func WeChatAuth(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setupLogin(&user, c)
|
controller.SetupLogin(&user, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func WeChatBind(c *gin.Context) {
|
func WeChatBind(c *gin.Context) {
|
@ -23,6 +23,7 @@ func GetStatus(c *gin.Context) {
|
|||||||
"email_verification": config.EmailVerificationEnabled,
|
"email_verification": config.EmailVerificationEnabled,
|
||||||
"github_oauth": config.GitHubOAuthEnabled,
|
"github_oauth": config.GitHubOAuthEnabled,
|
||||||
"github_client_id": config.GitHubClientId,
|
"github_client_id": config.GitHubClientId,
|
||||||
|
"lark_client_id": config.LarkClientId,
|
||||||
"system_name": config.SystemName,
|
"system_name": config.SystemName,
|
||||||
"logo": config.Logo,
|
"logo": config.Logo,
|
||||||
"footer_html": config.Footer,
|
"footer_html": config.Footer,
|
||||||
|
@ -58,11 +58,11 @@ func Login(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setupLogin(&user, c)
|
SetupLogin(&user, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup session & cookies and then return user info
|
// setup session & cookies and then return user info
|
||||||
func setupLogin(user *model.User, c *gin.Context) {
|
func SetupLogin(user *model.User, c *gin.Context) {
|
||||||
session := sessions.Default(c)
|
session := sessions.Default(c)
|
||||||
session.Set("id", user.Id)
|
session.Set("id", user.Id)
|
||||||
session.Set("username", user.Username)
|
session.Set("username", user.Username)
|
||||||
|
@ -172,6 +172,10 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
config.GitHubClientId = value
|
config.GitHubClientId = value
|
||||||
case "GitHubClientSecret":
|
case "GitHubClientSecret":
|
||||||
config.GitHubClientSecret = value
|
config.GitHubClientSecret = value
|
||||||
|
case "LarkClientId":
|
||||||
|
config.LarkClientId = value
|
||||||
|
case "LarkClientSecret":
|
||||||
|
config.LarkClientSecret = value
|
||||||
case "Footer":
|
case "Footer":
|
||||||
config.Footer = value
|
config.Footer = value
|
||||||
case "SystemName":
|
case "SystemName":
|
||||||
|
@ -24,6 +24,7 @@ type User struct {
|
|||||||
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"`
|
||||||
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
||||||
|
LarkId string `json:"lark_id" gorm:"column:lark_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
|
||||||
Quota int64 `json:"quota" gorm:"bigint;default:0"`
|
Quota int64 `json:"quota" gorm:"bigint;default:0"`
|
||||||
@ -41,21 +42,21 @@ func GetMaxUserId() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetAllUsers(startIdx int, num int, order string) (users []*User, err error) {
|
func GetAllUsers(startIdx int, num int, order string) (users []*User, err error) {
|
||||||
query := DB.Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted)
|
query := DB.Limit(num).Offset(startIdx).Omit("password").Where("status != ?", common.UserStatusDeleted)
|
||||||
|
|
||||||
switch order {
|
switch order {
|
||||||
case "quota":
|
case "quota":
|
||||||
query = query.Order("quota desc")
|
query = query.Order("quota desc")
|
||||||
case "used_quota":
|
case "used_quota":
|
||||||
query = query.Order("used_quota desc")
|
query = query.Order("used_quota desc")
|
||||||
case "request_count":
|
case "request_count":
|
||||||
query = query.Order("request_count desc")
|
query = query.Order("request_count desc")
|
||||||
default:
|
default:
|
||||||
query = query.Order("id desc")
|
query = query.Order("id desc")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = query.Find(&users).Error
|
err = query.Find(&users).Error
|
||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchUsers(keyword string) (users []*User, err error) {
|
func SearchUsers(keyword string) (users []*User, err error) {
|
||||||
@ -206,6 +207,14 @@ func (user *User) FillUserByGitHubId() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (user *User) FillUserByLarkId() error {
|
||||||
|
if user.LarkId == "" {
|
||||||
|
return errors.New("lark id 为空!")
|
||||||
|
}
|
||||||
|
DB.Where(User{LarkId: user.LarkId}).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 为空!")
|
||||||
@ -234,6 +243,10 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsLarkIdAlreadyTaken(githubId string) bool {
|
||||||
|
return DB.Where("lark_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
func IsUsernameAlreadyTaken(username string) bool {
|
func IsUsernameAlreadyTaken(username string) bool {
|
||||||
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
|
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package router
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/songquanpeng/one-api/controller"
|
"github.com/songquanpeng/one-api/controller"
|
||||||
|
"github.com/songquanpeng/one-api/controller/auth"
|
||||||
"github.com/songquanpeng/one-api/middleware"
|
"github.com/songquanpeng/one-api/middleware"
|
||||||
|
|
||||||
"github.com/gin-contrib/gzip"
|
"github.com/gin-contrib/gzip"
|
||||||
@ -21,10 +22,11 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
||||||
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(), auth.GitHubOAuth)
|
||||||
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode)
|
apiRouter.GET("/oauth/lark", middleware.CriticalRateLimit(), auth.LarkOAuth)
|
||||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), auth.GenerateOAuthCode)
|
||||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), auth.WeChatAuth)
|
||||||
|
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), auth.WeChatBind)
|
||||||
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
apiRouter.GET("/oauth/email/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.EmailBind)
|
||||||
apiRouter.POST("/topup", middleware.AdminAuth(), controller.AdminTopUp)
|
apiRouter.POST("/topup", middleware.AdminAuth(), controller.AdminTopUp)
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import EditRedemption from './pages/Redemption/EditRedemption';
|
|||||||
import TopUp from './pages/TopUp';
|
import TopUp from './pages/TopUp';
|
||||||
import Log from './pages/Log';
|
import Log from './pages/Log';
|
||||||
import Chat from './pages/Chat';
|
import Chat from './pages/Chat';
|
||||||
|
import LarkOAuth from './components/LarkOAuth';
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const About = lazy(() => import('./pages/About'));
|
const About = lazy(() => import('./pages/About'));
|
||||||
@ -239,6 +240,14 @@ function App() {
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/oauth/lark'
|
||||||
|
element={
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<LarkOAuth />
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/setting'
|
path='/setting'
|
||||||
element={
|
element={
|
||||||
|
58
web/default/src/components/LarkOAuth.js
Normal file
58
web/default/src/components/LarkOAuth.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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 LarkOAuth = () => {
|
||||||
|
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, state, count) => {
|
||||||
|
const res = await API.get(`/api/oauth/lark?code=${code}&state=${state}`);
|
||||||
|
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 lark
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, count * 2000));
|
||||||
|
await sendCode(code, state, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let code = searchParams.get('code');
|
||||||
|
let state = searchParams.get('state');
|
||||||
|
sendCode(code, state, 0).then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment style={{ minHeight: '300px' }}>
|
||||||
|
<Dimmer active inverted>
|
||||||
|
<Loader size='large'>{prompt}</Loader>
|
||||||
|
</Dimmer>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LarkOAuth;
|
@ -3,7 +3,8 @@ import { Button, Divider, Form, Grid, Header, Image, Message, Modal, Segment } f
|
|||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
import { API, getLogo, showError, showSuccess, showWarning } from '../helpers';
|
import { API, getLogo, showError, showSuccess, showWarning } from '../helpers';
|
||||||
import { onGitHubOAuthClicked } from './utils';
|
import { onGitHubOAuthClicked, onLarkOAuthClicked } from './utils';
|
||||||
|
import larkIcon from '../images/lark.svg';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
@ -124,7 +125,7 @@ const LoginForm = () => {
|
|||||||
点击注册
|
点击注册
|
||||||
</Link>
|
</Link>
|
||||||
</Message>
|
</Message>
|
||||||
{status.github_oauth || status.wechat_login ? (
|
{status.github_oauth || status.wechat_login || status.lark_client_id ? (
|
||||||
<>
|
<>
|
||||||
<Divider horizontal>Or</Divider>
|
<Divider horizontal>Or</Divider>
|
||||||
{status.github_oauth ? (
|
{status.github_oauth ? (
|
||||||
@ -137,6 +138,18 @@ const LoginForm = () => {
|
|||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
{status.lark_client_id ? (
|
||||||
|
<Button
|
||||||
|
// circular
|
||||||
|
color=''
|
||||||
|
onClick={() => onLarkOAuthClicked(status.lark_client_id)}
|
||||||
|
style={{ padding: 0, width: 36, height: 36 }}
|
||||||
|
>
|
||||||
|
<img src={larkIcon} width={36} height={36} />
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<></>
|
||||||
|
)}
|
||||||
{status.wechat_login ? (
|
{status.wechat_login ? (
|
||||||
<Button
|
<Button
|
||||||
circular
|
circular
|
||||||
|
@ -4,7 +4,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
import { onGitHubOAuthClicked } from './utils';
|
import { onGitHubOAuthClicked, onLarkOAuthClicked } from './utils';
|
||||||
|
|
||||||
const PersonalSetting = () => {
|
const PersonalSetting = () => {
|
||||||
const [userState, userDispatch] = useContext(UserContext);
|
const [userState, userDispatch] = useContext(UserContext);
|
||||||
@ -247,6 +247,11 @@ const PersonalSetting = () => {
|
|||||||
<Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button>
|
<Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
status.lark_client_id && (
|
||||||
|
<Button onClick={()=>{onLarkOAuthClicked(status.lark_client_id)}}>绑定飞书账号</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowEmailBindModal(true);
|
setShowEmailBindModal(true);
|
||||||
|
@ -10,6 +10,8 @@ const SystemSetting = () => {
|
|||||||
GitHubOAuthEnabled: '',
|
GitHubOAuthEnabled: '',
|
||||||
GitHubClientId: '',
|
GitHubClientId: '',
|
||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
|
LarkClientId: '',
|
||||||
|
LarkClientSecret: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '',
|
SMTPPort: '',
|
||||||
@ -109,6 +111,8 @@ const SystemSetting = () => {
|
|||||||
name === 'ServerAddress' ||
|
name === 'ServerAddress' ||
|
||||||
name === 'GitHubClientId' ||
|
name === 'GitHubClientId' ||
|
||||||
name === 'GitHubClientSecret' ||
|
name === 'GitHubClientSecret' ||
|
||||||
|
name === 'LarkClientId' ||
|
||||||
|
name === 'LarkClientSecret' ||
|
||||||
name === 'WeChatServerAddress' ||
|
name === 'WeChatServerAddress' ||
|
||||||
name === 'WeChatServerToken' ||
|
name === 'WeChatServerToken' ||
|
||||||
name === 'WeChatAccountQRCodeImageURL' ||
|
name === 'WeChatAccountQRCodeImageURL' ||
|
||||||
@ -212,6 +216,18 @@ const SystemSetting = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitLarkOAuth = async () => {
|
||||||
|
if (originInputs['LarkClientId'] !== inputs.LarkClientId) {
|
||||||
|
await updateOption('LarkClientId', inputs.LarkClientId);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
originInputs['LarkClientSecret'] !== inputs.LarkClientSecret &&
|
||||||
|
inputs.LarkClientSecret !== ''
|
||||||
|
) {
|
||||||
|
await updateOption('LarkClientSecret', inputs.LarkClientSecret);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
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);
|
||||||
@ -469,6 +485,44 @@ const SystemSetting = () => {
|
|||||||
保存 GitHub OAuth 设置
|
保存 GitHub OAuth 设置
|
||||||
</Form.Button>
|
</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
|
<Header as='h3'>
|
||||||
|
配置飞书授权登录
|
||||||
|
<Header.Subheader>
|
||||||
|
用以支持通过飞书进行登录注册,
|
||||||
|
<a href='https://open.feishu.cn/app' target='_blank'>
|
||||||
|
点击此处
|
||||||
|
</a>
|
||||||
|
管理你的飞书应用
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Message>
|
||||||
|
主页链接填 <code>{inputs.ServerAddress}</code>
|
||||||
|
,重定向 URL 填{' '}
|
||||||
|
<code>{`${inputs.ServerAddress}/oauth/lark`}</code>
|
||||||
|
</Message>
|
||||||
|
<Form.Group widths={3}>
|
||||||
|
<Form.Input
|
||||||
|
label='App ID'
|
||||||
|
name='LarkClientId'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.LarkClientId}
|
||||||
|
placeholder='输入 App ID'
|
||||||
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='App Secret'
|
||||||
|
name='LarkClientSecret'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
type='password'
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.LarkClientSecret}
|
||||||
|
placeholder='敏感信息不会发送到前端显示'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitLarkOAuth}>
|
||||||
|
保存飞书 OAuth 设置
|
||||||
|
</Form.Button>
|
||||||
|
<Divider />
|
||||||
<Header as='h3'>
|
<Header as='h3'>
|
||||||
配置 WeChat Server
|
配置 WeChat Server
|
||||||
<Header.Subheader>
|
<Header.Subheader>
|
||||||
|
@ -18,3 +18,12 @@ export async function onGitHubOAuthClicked(github_client_id) {
|
|||||||
`https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`
|
`https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function onLarkOAuthClicked(lark_client_id) {
|
||||||
|
const state = await getOAuthState();
|
||||||
|
if (!state) return;
|
||||||
|
let redirect_uri = `${window.location.origin}/oauth/lark`;
|
||||||
|
window.open(
|
||||||
|
`https://open.feishu.cn/open-apis/authen/v1/index?redirect_uri=${redirect_uri}&app_id=${lark_client_id}&state=${state}`
|
||||||
|
);
|
||||||
|
}
|
1
web/default/src/images/lark.svg
Normal file
1
web/default/src/images/lark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.4 KiB |
Loading…
Reference in New Issue
Block a user