✨ feat: add Lark OAuth (#149)
This commit is contained in:
parent
3c7c13758b
commit
9ccf1381e8
@ -70,6 +70,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 = ""
|
||||||
|
267
controller/lark.go
Normal file
267
controller/lark.go
Normal file
@ -0,0 +1,267 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LarkAppAccessTokenResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
AppAccessToken string `json:"app_access_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LarkUserAccessTokenResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LarkUser struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
OpenID string `json:"open_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLarkAppAccessToken() (string, error) {
|
||||||
|
values := map[string]string{
|
||||||
|
"app_id": common.LarkClientId,
|
||||||
|
"app_secret": common.LarkClientSecret,
|
||||||
|
}
|
||||||
|
jsonData, err := json.Marshal(values)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", "https://open.feishu.cn/open-apis/auth/v3/app_access_token/internal/", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return "", 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 {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return "", errors.New("无法连接至飞书服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
var appAccessTokenResponse LarkAppAccessTokenResponse
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&appAccessTokenResponse)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if appAccessTokenResponse.Code != 0 {
|
||||||
|
return "", errors.New(appAccessTokenResponse.Msg)
|
||||||
|
}
|
||||||
|
return appAccessTokenResponse.AppAccessToken, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLarkUserAccessToken(code string) (string, error) {
|
||||||
|
appAccessToken, err := getLarkAppAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
values := map[string]string{
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"code": code,
|
||||||
|
}
|
||||||
|
jsonData, err := json.Marshal(values)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", "https://open.feishu.cn/open-apis/authen/v1/oidc/access_token", bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appAccessToken))
|
||||||
|
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 {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return "", errors.New("无法连接至飞书服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
var larkUserAccessTokenResponse LarkUserAccessTokenResponse
|
||||||
|
err = json.NewDecoder(res.Body).Decode(&larkUserAccessTokenResponse)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if larkUserAccessTokenResponse.Code != 0 {
|
||||||
|
return "", errors.New(larkUserAccessTokenResponse.Msg)
|
||||||
|
}
|
||||||
|
return larkUserAccessTokenResponse.Data.AccessToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLarkUserInfoByCode(code string) (*LarkUser, error) {
|
||||||
|
if code == "" {
|
||||||
|
return nil, errors.New("无效的参数")
|
||||||
|
}
|
||||||
|
|
||||||
|
userAccessToken, err := getLarkUserAccessToken(code)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", "https://open.feishu.cn/open-apis/authen/v1/user_info", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", userAccessToken))
|
||||||
|
client := http.Client{
|
||||||
|
Timeout: 5 * time.Second,
|
||||||
|
}
|
||||||
|
res2, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog(err.Error())
|
||||||
|
return nil, errors.New("无法连接至飞书服务器,请稍后重试!")
|
||||||
|
}
|
||||||
|
var larkUser LarkUser
|
||||||
|
err = json.NewDecoder(res2.Body).Decode(&larkUser)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Println("larkUser", larkUser)
|
||||||
|
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.Data.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 common.RegisterEnabled {
|
||||||
|
user.Username = "lark_" + strconv.Itoa(model.GetMaxUserId()+1)
|
||||||
|
if larkUser.Data.Name != "" {
|
||||||
|
user.DisplayName = larkUser.Data.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
|
||||||
|
}
|
||||||
|
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.Data.OpenID,
|
||||||
|
}
|
||||||
|
if model.IsLarkIdAlreadyTaken(user.LarkId) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "该飞书账户已被绑定",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
session := sessions.Default(c)
|
||||||
|
id := session.Get("id")
|
||||||
|
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.Data.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",
|
||||||
|
})
|
||||||
|
}
|
@ -28,6 +28,7 @@ func GetStatus(c *gin.Context) {
|
|||||||
"email_verification": common.EmailVerificationEnabled,
|
"email_verification": common.EmailVerificationEnabled,
|
||||||
"github_oauth": common.GitHubOAuthEnabled,
|
"github_oauth": common.GitHubOAuthEnabled,
|
||||||
"github_client_id": common.GitHubClientId,
|
"github_client_id": common.GitHubClientId,
|
||||||
|
"lark_client_id": common.LarkClientId,
|
||||||
"system_name": common.SystemName,
|
"system_name": common.SystemName,
|
||||||
"logo": common.Logo,
|
"logo": common.Logo,
|
||||||
"footer_html": common.Footer,
|
"footer_html": common.Footer,
|
||||||
|
@ -166,6 +166,8 @@ var optionStringMap = map[string]*string{
|
|||||||
"TurnstileSecretKey": &common.TurnstileSecretKey,
|
"TurnstileSecretKey": &common.TurnstileSecretKey,
|
||||||
"TopUpLink": &common.TopUpLink,
|
"TopUpLink": &common.TopUpLink,
|
||||||
"ChatLink": &common.ChatLink,
|
"ChatLink": &common.ChatLink,
|
||||||
|
"LarkClientId": &common.LarkClientId,
|
||||||
|
"LarkClientSecret": &common.LarkClientSecret,
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateOptionMap(key string, value string) (err error) {
|
func updateOptionMap(key string, value string) (err error) {
|
||||||
|
@ -22,6 +22,7 @@ type User struct {
|
|||||||
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"`
|
||||||
TelegramId int64 `json:"telegram_id" gorm:"bigint,column:telegram_id;default:0;"`
|
TelegramId int64 `json:"telegram_id" gorm:"bigint,column:telegram_id;default:0;"`
|
||||||
|
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 int `json:"quota" gorm:"type:int;default:0"`
|
Quota int `json:"quota" gorm:"type:int;default:0"`
|
||||||
@ -248,6 +249,14 @@ func (user *User) FillUserByWeChatId() 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) FillUserByUsername() error {
|
func (user *User) FillUserByUsername() error {
|
||||||
if user.Username == "" {
|
if user.Username == "" {
|
||||||
return errors.New("username 为空!")
|
return errors.New("username 为空!")
|
||||||
@ -268,6 +277,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 IsTelegramIdAlreadyTaken(telegramId int64) bool {
|
func IsTelegramIdAlreadyTaken(telegramId int64) bool {
|
||||||
return DB.Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1
|
return DB.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/lark", middleware.CriticalRateLimit(), controller.LarkOAuth)
|
||||||
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)
|
||||||
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
apiRouter.GET("/oauth/wechat/bind", middleware.CriticalRateLimit(), middleware.UserAuth(), controller.WeChatBind)
|
||||||
|
1
web/src/assets/images/icons/lark.svg
Normal file
1
web/src/assets/images/icons/lark.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 5.4 KiB |
@ -48,6 +48,28 @@ const useLogin = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const larkLogin = async (code, state) => {
|
||||||
|
try {
|
||||||
|
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('/panel');
|
||||||
|
} else {
|
||||||
|
dispatch({ type: LOGIN, payload: data });
|
||||||
|
localStorage.setItem('user', JSON.stringify(data));
|
||||||
|
showSuccess('登录成功!');
|
||||||
|
navigate('/panel');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success, message };
|
||||||
|
} catch (err) {
|
||||||
|
// 请求失败,设置错误信息
|
||||||
|
return { success: false, message: '' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const wechatLogin = async (code) => {
|
const wechatLogin = async (code) => {
|
||||||
try {
|
try {
|
||||||
const res = await API.get(`/api/oauth/wechat?code=${code}`);
|
const res = await API.get(`/api/oauth/wechat?code=${code}`);
|
||||||
@ -72,7 +94,7 @@ const useLogin = () => {
|
|||||||
navigate('/');
|
navigate('/');
|
||||||
};
|
};
|
||||||
|
|
||||||
return { login, logout, githubLogin, wechatLogin };
|
return { login, logout, githubLogin, wechatLogin, larkLogin };
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useLogin;
|
export default useLogin;
|
||||||
|
@ -8,6 +8,7 @@ import MinimalLayout from 'layout/MinimalLayout';
|
|||||||
const AuthLogin = Loadable(lazy(() => import('views/Authentication/Auth/Login')));
|
const AuthLogin = Loadable(lazy(() => import('views/Authentication/Auth/Login')));
|
||||||
const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register')));
|
const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register')));
|
||||||
const GitHubOAuth = Loadable(lazy(() => import('views/Authentication/Auth/GitHubOAuth')));
|
const GitHubOAuth = Loadable(lazy(() => import('views/Authentication/Auth/GitHubOAuth')));
|
||||||
|
const LarkOAuth = Loadable(lazy(() => import('views/Authentication/Auth/LarkOAuth')));
|
||||||
const ForgetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ForgetPassword')));
|
const ForgetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ForgetPassword')));
|
||||||
const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword')));
|
const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword')));
|
||||||
const Home = Loadable(lazy(() => import('views/Home')));
|
const Home = Loadable(lazy(() => import('views/Home')));
|
||||||
@ -49,6 +50,10 @@ const OtherRoutes = {
|
|||||||
path: '/oauth/github',
|
path: '/oauth/github',
|
||||||
element: <GitHubOAuth />
|
element: <GitHubOAuth />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/oauth/lark',
|
||||||
|
element: <LarkOAuth />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/404',
|
path: '/404',
|
||||||
element: <NotFoundView />
|
element: <NotFoundView />
|
||||||
|
@ -106,6 +106,13 @@ export async function onGitHubOAuthClicked(github_client_id, openInNewTab = fals
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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/authorize?redirect_uri=${redirect_uri}&app_id=${lark_client_id}&state=${state}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function isAdmin() {
|
export function isAdmin() {
|
||||||
let user = localStorage.getItem('user');
|
let user = localStorage.getItem('user');
|
||||||
if (!user) return false;
|
if (!user) return false;
|
||||||
|
95
web/src/views/Authentication/Auth/LarkOAuth.js
Normal file
95
web/src/views/Authentication/Auth/LarkOAuth.js
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { showError } from 'utils/common';
|
||||||
|
import useLogin from 'hooks/useLogin';
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { Grid, Stack, Typography, useMediaQuery, CircularProgress } from '@mui/material';
|
||||||
|
|
||||||
|
// project imports
|
||||||
|
import AuthWrapper from '../AuthWrapper';
|
||||||
|
import AuthCardWrapper from '../AuthCardWrapper';
|
||||||
|
import Logo from 'ui-component/Logo';
|
||||||
|
|
||||||
|
// assets
|
||||||
|
|
||||||
|
// ================================|| AUTH3 - LOGIN ||================================ //
|
||||||
|
|
||||||
|
const LarkOAuth = () => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const [prompt, setPrompt] = useState('处理中...');
|
||||||
|
const { larkLogin } = useLogin();
|
||||||
|
|
||||||
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
const sendCode = async (code, state, count) => {
|
||||||
|
const { success, message } = await larkLogin(code, state);
|
||||||
|
if (!success) {
|
||||||
|
if (message) {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
if (count === 0) {
|
||||||
|
setPrompt(`操作失败,重定向至登录界面中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
navigate('/login');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
count++;
|
||||||
|
setPrompt(`出现错误,第 ${count} 次重试中...`);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
await sendCode(code, state, count);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let code = searchParams.get('code');
|
||||||
|
let state = searchParams.get('state');
|
||||||
|
sendCode(code, state, 0).then();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthWrapper>
|
||||||
|
<Grid container direction="column" justifyContent="flex-end">
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Grid container justifyContent="center" alignItems="center" sx={{ minHeight: 'calc(100vh - 136px)' }}>
|
||||||
|
<Grid item sx={{ m: { xs: 1, sm: 3 }, mb: 0 }}>
|
||||||
|
<AuthCardWrapper>
|
||||||
|
<Grid container spacing={2} alignItems="center" justifyContent="center">
|
||||||
|
<Grid item sx={{ mb: 3 }}>
|
||||||
|
<Link to="#">
|
||||||
|
<Logo />
|
||||||
|
</Link>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Grid container direction={matchDownSM ? 'column-reverse' : 'row'} alignItems="center" justifyContent="center">
|
||||||
|
<Grid item>
|
||||||
|
<Stack alignItems="center" justifyContent="center" spacing={1}>
|
||||||
|
<Typography color={theme.palette.primary.main} gutterBottom variant={matchDownSM ? 'h3' : 'h2'}>
|
||||||
|
飞书 登录
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} container direction="column" justifyContent="center" alignItems="center" style={{ height: '200px' }}>
|
||||||
|
<CircularProgress />
|
||||||
|
<Typography variant="h3" paddingTop={'20px'}>
|
||||||
|
{prompt}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</AuthCardWrapper>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</AuthWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LarkOAuth;
|
@ -35,7 +35,8 @@ import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|||||||
|
|
||||||
import Github from 'assets/images/icons/github.svg';
|
import Github from 'assets/images/icons/github.svg';
|
||||||
import Wechat from 'assets/images/icons/wechat.svg';
|
import Wechat from 'assets/images/icons/wechat.svg';
|
||||||
import { onGitHubOAuthClicked } from 'utils/common';
|
import Lark from 'assets/images/icons/lark.svg';
|
||||||
|
import { onGitHubOAuthClicked, onLarkOAuthClicked } from 'utils/common';
|
||||||
|
|
||||||
// ============================|| FIREBASE - LOGIN ||============================ //
|
// ============================|| FIREBASE - LOGIN ||============================ //
|
||||||
|
|
||||||
@ -49,7 +50,7 @@ const LoginForm = ({ ...others }) => {
|
|||||||
// const [checked, setChecked] = useState(true);
|
// const [checked, setChecked] = useState(true);
|
||||||
|
|
||||||
let tripartiteLogin = false;
|
let tripartiteLogin = false;
|
||||||
if (siteInfo.github_oauth || siteInfo.wechat_login) {
|
if (siteInfo.github_oauth || siteInfo.wechat_login || siteInfo.lark_client_id) {
|
||||||
tripartiteLogin = true;
|
tripartiteLogin = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,6 +122,29 @@ const LoginForm = ({ ...others }) => {
|
|||||||
<WechatModal open={openWechat} handleClose={handleWechatClose} wechatLogin={wechatLogin} qrCode={siteInfo.wechat_qrcode} />
|
<WechatModal open={openWechat} handleClose={handleWechatClose} wechatLogin={wechatLogin} qrCode={siteInfo.wechat_qrcode} />
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
{siteInfo.lark_client_id && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<AnimateButton>
|
||||||
|
<Button
|
||||||
|
disableElevation
|
||||||
|
fullWidth
|
||||||
|
onClick={() => onLarkOAuthClicked(siteInfo.lark_client_id)}
|
||||||
|
size="large"
|
||||||
|
variant="outlined"
|
||||||
|
sx={{
|
||||||
|
color: 'grey.700',
|
||||||
|
backgroundColor: theme.palette.grey[50],
|
||||||
|
borderColor: theme.palette.grey[100]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ mr: { xs: 1, sm: 2, width: 20 }, display: 'flex', alignItems: 'center' }}>
|
||||||
|
<img src={Lark} alt="Lark" width={25} height={25} style={{ marginRight: matchDownSM ? 8 : 16 }} />
|
||||||
|
</Box>
|
||||||
|
使用飞书登录
|
||||||
|
</Button>
|
||||||
|
</AnimateButton>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -1,17 +1,32 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import UserCard from 'ui-component/cards/UserCard';
|
import UserCard from 'ui-component/cards/UserCard';
|
||||||
import { Card, Button, InputLabel, FormControl, OutlinedInput, Stack, Alert, Divider, Chip, Typography } from '@mui/material';
|
import {
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
InputLabel,
|
||||||
|
FormControl,
|
||||||
|
OutlinedInput,
|
||||||
|
Stack,
|
||||||
|
Alert,
|
||||||
|
Divider,
|
||||||
|
Chip,
|
||||||
|
Typography,
|
||||||
|
SvgIcon,
|
||||||
|
useMediaQuery
|
||||||
|
} from '@mui/material';
|
||||||
import Grid from '@mui/material/Unstable_Grid2';
|
import Grid from '@mui/material/Unstable_Grid2';
|
||||||
import SubCard from 'ui-component/cards/SubCard';
|
import SubCard from 'ui-component/cards/SubCard';
|
||||||
import { IconBrandWechat, IconBrandGithub, IconMail, IconBrandTelegram } from '@tabler/icons-react';
|
import { IconBrandWechat, IconBrandGithub, IconMail, IconBrandTelegram } from '@tabler/icons-react';
|
||||||
import Label from 'ui-component/Label';
|
import Label from 'ui-component/Label';
|
||||||
import { API } from 'utils/api';
|
import { API } from 'utils/api';
|
||||||
import { showError, showSuccess, onGitHubOAuthClicked, copy, trims } from 'utils/common';
|
import { showError, showSuccess, onGitHubOAuthClicked, copy, trims, onLarkOAuthClicked } from 'utils/common';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import WechatModal from 'views/Authentication/AuthForms/WechatModal';
|
import WechatModal from 'views/Authentication/AuthForms/WechatModal';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import EmailModal from './component/EmailModal';
|
import EmailModal from './component/EmailModal';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
|
import { ReactComponent as Lark } from 'assets/images/icons/lark.svg';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
const validationSchema = Yup.object().shape({
|
const validationSchema = Yup.object().shape({
|
||||||
username: Yup.string().required('用户名 不能为空').min(3, '用户名 不能小于 3 个字符'),
|
username: Yup.string().required('用户名 不能为空').min(3, '用户名 不能小于 3 个字符'),
|
||||||
@ -29,6 +44,8 @@ export default function Profile() {
|
|||||||
const [openWechat, setOpenWechat] = useState(false);
|
const [openWechat, setOpenWechat] = useState(false);
|
||||||
const [openEmail, setOpenEmail] = useState(false);
|
const [openEmail, setOpenEmail] = useState(false);
|
||||||
const status = useSelector((state) => state.siteInfo);
|
const status = useSelector((state) => state.siteInfo);
|
||||||
|
const theme = useTheme();
|
||||||
|
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
const handleWechatOpen = () => {
|
const handleWechatOpen = () => {
|
||||||
setOpenWechat(true);
|
setOpenWechat(true);
|
||||||
@ -120,7 +137,13 @@ export default function Profile() {
|
|||||||
<UserCard>
|
<UserCard>
|
||||||
<Card sx={{ paddingTop: '20px' }}>
|
<Card sx={{ paddingTop: '20px' }}>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
<Stack direction="row" alignItems="center" justifyContent="center" spacing={2} sx={{ paddingBottom: '20px' }}>
|
<Stack
|
||||||
|
direction={matchDownSM ? 'column' : 'row'}
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="center"
|
||||||
|
spacing={2}
|
||||||
|
sx={{ paddingBottom: '20px' }}
|
||||||
|
>
|
||||||
<Label variant="ghost" color={inputs.wechat_id ? 'primary' : 'default'}>
|
<Label variant="ghost" color={inputs.wechat_id ? 'primary' : 'default'}>
|
||||||
<IconBrandWechat /> {inputs.wechat_id || '未绑定'}
|
<IconBrandWechat /> {inputs.wechat_id || '未绑定'}
|
||||||
</Label>
|
</Label>
|
||||||
@ -133,6 +156,9 @@ export default function Profile() {
|
|||||||
<Label variant="ghost" color={inputs.telegram_id ? 'primary' : 'default'}>
|
<Label variant="ghost" color={inputs.telegram_id ? 'primary' : 'default'}>
|
||||||
<IconBrandTelegram /> {inputs.telegram_id || '未绑定'}
|
<IconBrandTelegram /> {inputs.telegram_id || '未绑定'}
|
||||||
</Label>
|
</Label>
|
||||||
|
<Label variant="ghost" color={inputs.lark_id ? 'primary' : 'default'}>
|
||||||
|
<SvgIcon component={Lark} inheritViewBox="0 0 24 24" /> {inputs.lark_id || '未绑定'}
|
||||||
|
</Label>
|
||||||
</Stack>
|
</Stack>
|
||||||
<SubCard title="个人信息">
|
<SubCard title="个人信息">
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
@ -202,6 +228,14 @@ export default function Profile() {
|
|||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{status.lark_client_id && !inputs.lark_id && (
|
||||||
|
<Grid xs={12} md={4}>
|
||||||
|
<Button variant="contained" onClick={() => onLarkOAuthClicked(status.lark_client_id)}>
|
||||||
|
绑定 飞书 账号
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
|
||||||
<Grid xs={12} md={4}>
|
<Grid xs={12} md={4}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
|
@ -31,6 +31,8 @@ const SystemSetting = () => {
|
|||||||
GitHubOAuthEnabled: '',
|
GitHubOAuthEnabled: '',
|
||||||
GitHubClientId: '',
|
GitHubClientId: '',
|
||||||
GitHubClientSecret: '',
|
GitHubClientSecret: '',
|
||||||
|
LarkClientId: '',
|
||||||
|
LarkClientSecret: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
SMTPServer: '',
|
SMTPServer: '',
|
||||||
SMTPPort: '',
|
SMTPPort: '',
|
||||||
@ -144,7 +146,9 @@ const SystemSetting = () => {
|
|||||||
name === 'WeChatAccountQRCodeImageURL' ||
|
name === 'WeChatAccountQRCodeImageURL' ||
|
||||||
name === 'TurnstileSiteKey' ||
|
name === 'TurnstileSiteKey' ||
|
||||||
name === 'TurnstileSecretKey' ||
|
name === 'TurnstileSecretKey' ||
|
||||||
name === 'EmailDomainWhitelist'
|
name === 'EmailDomainWhitelist' ||
|
||||||
|
name === 'LarkClientId' ||
|
||||||
|
name === 'LarkClientSecret'
|
||||||
) {
|
) {
|
||||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||||
} else {
|
} else {
|
||||||
@ -209,6 +213,15 @@ 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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack spacing={2}>
|
<Stack spacing={2}>
|
||||||
@ -545,6 +558,61 @@ const SystemSetting = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</SubCard>
|
</SubCard>
|
||||||
|
<SubCard
|
||||||
|
title="配置飞书授权登录"
|
||||||
|
subTitle={
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
用以支持通过飞书进行登录注册,
|
||||||
|
<a href="https://open.feishu.cn/app" target="_blank" rel="noreferrer">
|
||||||
|
点击此处
|
||||||
|
</a>
|
||||||
|
管理你的飞书应用
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Grid container spacing={{ xs: 3, sm: 2, md: 4 }}>
|
||||||
|
<Grid xs={12}>
|
||||||
|
<Alert severity="info" sx={{ wordWrap: 'break-word' }}>
|
||||||
|
主页链接填 <code>{inputs.ServerAddress}</code>
|
||||||
|
,重定向 URL 填 <code>{`${inputs.ServerAddress}/oauth/lark`}</code>
|
||||||
|
</Alert>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={12} md={6}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="LarkClientId">App ID</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="LarkClientId"
|
||||||
|
name="LarkClientId"
|
||||||
|
value={inputs.LarkClientId || ''}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
label="App ID"
|
||||||
|
placeholder="输入 App ID"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={12} md={6}>
|
||||||
|
<FormControl fullWidth>
|
||||||
|
<InputLabel htmlFor="LarkClientSecret">App Secret</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="LarkClientSecret"
|
||||||
|
name="LarkClientSecret"
|
||||||
|
value={inputs.LarkClientSecret || ''}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
label="App Secret"
|
||||||
|
placeholder="敏感信息不会发送到前端显示"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</Grid>
|
||||||
|
<Grid xs={12}>
|
||||||
|
<Button variant="contained" onClick={submitLarkOAuth}>
|
||||||
|
保存飞书 OAuth 设置
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</SubCard>
|
||||||
<SubCard
|
<SubCard
|
||||||
title="配置 Turnstile"
|
title="配置 Turnstile"
|
||||||
subTitle={
|
subTitle={
|
||||||
|
Loading…
Reference in New Issue
Block a user