feat: add Lark OAuth (#149)

This commit is contained in:
Martial BE
2024-04-16 13:03:05 +08:00
parent 3c7c13758b
commit 9ccf1381e8
14 changed files with 550 additions and 7 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.4 KiB

View File

@@ -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;

View File

@@ -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 />

View File

@@ -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;

View 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;

View File

@@ -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={{

View File

@@ -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"

View File

@@ -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={