✨ feat: add Lark OAuth (#149)
This commit is contained in:
parent
3c7c13758b
commit
9ccf1381e8
@ -70,6 +70,9 @@ var SMTPToken = ""
|
||||
var GitHubClientId = ""
|
||||
var GitHubClientSecret = ""
|
||||
|
||||
var LarkClientId = ""
|
||||
var LarkClientSecret = ""
|
||||
|
||||
var WeChatServerAddress = ""
|
||||
var WeChatServerToken = ""
|
||||
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,
|
||||
"github_oauth": common.GitHubOAuthEnabled,
|
||||
"github_client_id": common.GitHubClientId,
|
||||
"lark_client_id": common.LarkClientId,
|
||||
"system_name": common.SystemName,
|
||||
"logo": common.Logo,
|
||||
"footer_html": common.Footer,
|
||||
|
@ -166,6 +166,8 @@ var optionStringMap = map[string]*string{
|
||||
"TurnstileSecretKey": &common.TurnstileSecretKey,
|
||||
"TopUpLink": &common.TopUpLink,
|
||||
"ChatLink": &common.ChatLink,
|
||||
"LarkClientId": &common.LarkClientId,
|
||||
"LarkClientSecret": &common.LarkClientSecret,
|
||||
}
|
||||
|
||||
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"`
|
||||
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
|
||||
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!
|
||||
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"`
|
||||
@ -248,6 +249,14 @@ func (user *User) FillUserByWeChatId() error {
|
||||
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 {
|
||||
if user.Username == "" {
|
||||
return errors.New("username 为空!")
|
||||
@ -268,6 +277,10 @@ func IsGitHubIdAlreadyTaken(githubId string) bool {
|
||||
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 {
|
||||
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.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||
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/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||
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) => {
|
||||
try {
|
||||
const res = await API.get(`/api/oauth/wechat?code=${code}`);
|
||||
@ -72,7 +94,7 @@ const useLogin = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return { login, logout, githubLogin, wechatLogin };
|
||||
return { login, logout, githubLogin, wechatLogin, larkLogin };
|
||||
};
|
||||
|
||||
export default useLogin;
|
||||
|
@ -8,6 +8,7 @@ import MinimalLayout from 'layout/MinimalLayout';
|
||||
const AuthLogin = Loadable(lazy(() => import('views/Authentication/Auth/Login')));
|
||||
const AuthRegister = Loadable(lazy(() => import('views/Authentication/Auth/Register')));
|
||||
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 ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/ResetPassword')));
|
||||
const Home = Loadable(lazy(() => import('views/Home')));
|
||||
@ -49,6 +50,10 @@ const OtherRoutes = {
|
||||
path: '/oauth/github',
|
||||
element: <GitHubOAuth />
|
||||
},
|
||||
{
|
||||
path: '/oauth/lark',
|
||||
element: <LarkOAuth />
|
||||
},
|
||||
{
|
||||
path: '/404',
|
||||
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() {
|
||||
let user = localStorage.getItem('user');
|
||||
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 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 ||============================ //
|
||||
|
||||
@ -49,7 +50,7 @@ const LoginForm = ({ ...others }) => {
|
||||
// const [checked, setChecked] = useState(true);
|
||||
|
||||
let tripartiteLogin = false;
|
||||
if (siteInfo.github_oauth || siteInfo.wechat_login) {
|
||||
if (siteInfo.github_oauth || siteInfo.wechat_login || siteInfo.lark_client_id) {
|
||||
tripartiteLogin = true;
|
||||
}
|
||||
|
||||
@ -121,6 +122,29 @@ const LoginForm = ({ ...others }) => {
|
||||
<WechatModal open={openWechat} handleClose={handleWechatClose} wechatLogin={wechatLogin} qrCode={siteInfo.wechat_qrcode} />
|
||||
</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}>
|
||||
<Box
|
||||
sx={{
|
||||
|
@ -1,17 +1,32 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
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 SubCard from 'ui-component/cards/SubCard';
|
||||
import { IconBrandWechat, IconBrandGithub, IconMail, IconBrandTelegram } from '@tabler/icons-react';
|
||||
import Label from 'ui-component/Label';
|
||||
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 WechatModal from 'views/Authentication/AuthForms/WechatModal';
|
||||
import { useSelector } from 'react-redux';
|
||||
import EmailModal from './component/EmailModal';
|
||||
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({
|
||||
username: Yup.string().required('用户名 不能为空').min(3, '用户名 不能小于 3 个字符'),
|
||||
@ -29,6 +44,8 @@ export default function Profile() {
|
||||
const [openWechat, setOpenWechat] = useState(false);
|
||||
const [openEmail, setOpenEmail] = useState(false);
|
||||
const status = useSelector((state) => state.siteInfo);
|
||||
const theme = useTheme();
|
||||
const matchDownSM = useMediaQuery(theme.breakpoints.down('md'));
|
||||
|
||||
const handleWechatOpen = () => {
|
||||
setOpenWechat(true);
|
||||
@ -120,7 +137,13 @@ export default function Profile() {
|
||||
<UserCard>
|
||||
<Card sx={{ paddingTop: '20px' }}>
|
||||
<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'}>
|
||||
<IconBrandWechat /> {inputs.wechat_id || '未绑定'}
|
||||
</Label>
|
||||
@ -133,6 +156,9 @@ export default function Profile() {
|
||||
<Label variant="ghost" color={inputs.telegram_id ? 'primary' : 'default'}>
|
||||
<IconBrandTelegram /> {inputs.telegram_id || '未绑定'}
|
||||
</Label>
|
||||
<Label variant="ghost" color={inputs.lark_id ? 'primary' : 'default'}>
|
||||
<SvgIcon component={Lark} inheritViewBox="0 0 24 24" /> {inputs.lark_id || '未绑定'}
|
||||
</Label>
|
||||
</Stack>
|
||||
<SubCard title="个人信息">
|
||||
<Grid container spacing={2}>
|
||||
@ -202,6 +228,14 @@ export default function Profile() {
|
||||
</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}>
|
||||
<Button
|
||||
variant="contained"
|
||||
|
@ -31,6 +31,8 @@ const SystemSetting = () => {
|
||||
GitHubOAuthEnabled: '',
|
||||
GitHubClientId: '',
|
||||
GitHubClientSecret: '',
|
||||
LarkClientId: '',
|
||||
LarkClientSecret: '',
|
||||
Notice: '',
|
||||
SMTPServer: '',
|
||||
SMTPPort: '',
|
||||
@ -144,7 +146,9 @@ const SystemSetting = () => {
|
||||
name === 'WeChatAccountQRCodeImageURL' ||
|
||||
name === 'TurnstileSiteKey' ||
|
||||
name === 'TurnstileSecretKey' ||
|
||||
name === 'EmailDomainWhitelist'
|
||||
name === 'EmailDomainWhitelist' ||
|
||||
name === 'LarkClientId' ||
|
||||
name === 'LarkClientSecret'
|
||||
) {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
} 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 (
|
||||
<>
|
||||
<Stack spacing={2}>
|
||||
@ -545,6 +558,61 @@ const SystemSetting = () => {
|
||||
</Grid>
|
||||
</Grid>
|
||||
</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
|
||||
title="配置 Turnstile"
|
||||
subTitle={
|
||||
|
Loading…
Reference in New Issue
Block a user