chore: sync with gin-template

This commit is contained in:
JustSong
2022-11-22 11:04:06 +08:00
parent 8247192977
commit 5913266969
41 changed files with 813 additions and 490 deletions
+33 -15
View File
@@ -1,4 +1,4 @@
import React, { lazy, Suspense, useEffect } from 'react';
import React, { lazy, Suspense, useContext, useEffect } from 'react';
import { Route, Routes } from 'react-router-dom';
import Loading from './components/Loading';
import User from './pages/User';
@@ -9,34 +9,52 @@ import NotFound from './pages/NotFound';
import Setting from './pages/Setting';
import EditUser from './pages/User/EditUser';
import AddUser from './pages/User/AddUser';
import { API, showError } from './helpers';
import { API, showError, showNotice } from './helpers';
import PasswordResetForm from './components/PasswordResetForm';
import GitHubOAuth from './components/GitHubOAuth';
import PasswordResetConfirm from './components/PasswordResetConfirm';
import { UserContext } from './context/User';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
function App() {
const [userState, userDispatch] = useContext(UserContext);
const loadUser = () => {
let user = localStorage.getItem('user');
if (user) {
let data = JSON.parse(user);
userDispatch({ type: 'login', payload: data });
}
};
const loadStatus = async () => {
const res = await API.get('/api/status');
const { success, data } = res.data;
if (success) {
localStorage.setItem('status', JSON.stringify(data));
localStorage.setItem('footer_html', data.footer_html);
let currentVersion = localStorage.getItem('version');
if (currentVersion && currentVersion !== data.version) {
localStorage.setItem('version', data.version);
showNotice(
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
);
}
} else {
showError('无法正常连接至服务器!');
}
};
useEffect(() => {
loadUser();
loadStatus().then();
}, []);
return (
<Routes>
<Route
path="/"
path='/'
element={
<Suspense fallback={<Loading></Loading>}>
<Home />
@@ -44,7 +62,7 @@ function App() {
}
/>
<Route
path="/user"
path='/user'
element={
<PrivateRoute>
<User />
@@ -52,7 +70,7 @@ function App() {
}
/>
<Route
path="/user/edit/:id"
path='/user/edit/:id'
element={
<Suspense fallback={<Loading></Loading>}>
<EditUser />
@@ -60,7 +78,7 @@ function App() {
}
/>
<Route
path="/user/edit"
path='/user/edit'
element={
<Suspense fallback={<Loading></Loading>}>
<EditUser />
@@ -68,7 +86,7 @@ function App() {
}
/>
<Route
path="/user/add"
path='/user/add'
element={
<Suspense fallback={<Loading></Loading>}>
<AddUser />
@@ -76,7 +94,7 @@ function App() {
}
/>
<Route
path="/user/reset"
path='/user/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetConfirm />
@@ -84,7 +102,7 @@ function App() {
}
/>
<Route
path="/login"
path='/login'
element={
<Suspense fallback={<Loading></Loading>}>
<LoginForm />
@@ -92,7 +110,7 @@ function App() {
}
/>
<Route
path="/register"
path='/register'
element={
<Suspense fallback={<Loading></Loading>}>
<RegisterForm />
@@ -100,7 +118,7 @@ function App() {
}
/>
<Route
path="/reset"
path='/reset'
element={
<Suspense fallback={<Loading></Loading>}>
<PasswordResetForm />
@@ -108,7 +126,7 @@ function App() {
}
/>
<Route
path="/oauth/github"
path='/oauth/github'
element={
<Suspense fallback={<Loading></Loading>}>
<GitHubOAuth />
@@ -116,7 +134,7 @@ function App() {
}
/>
<Route
path="/setting"
path='/setting'
element={
<PrivateRoute>
<Suspense fallback={<Loading></Loading>}>
@@ -126,14 +144,14 @@ function App() {
}
/>
<Route
path="/about"
path='/about'
element={
<Suspense fallback={<Loading></Loading>}>
<About />
</Suspense>
}
/>
<Route path="*" element={NotFound} />
<Route path='*' element={NotFound} />
</Routes>
);
}
+7 -7
View File
@@ -12,27 +12,27 @@ const Footer = () => {
return (
<Segment vertical>
<Container textAlign="center">
<Container textAlign='center'>
{Footer === '' ? (
<div className="custom-footer">
<div className='custom-footer'>
<a
href="https://github.com/songquanpeng/message-pusher"
target="_blank"
href='https://github.com/songquanpeng/message-pusher'
target='_blank'
>
消息推送服务 {process.env.REACT_APP_VERSION}{' '}
</a>
{' '}
<a href="https://github.com/songquanpeng" target="_blank">
<a href='https://github.com/songquanpeng' target='_blank'>
JustSong
</a>{' '}
构建源代码遵循{' '}
<a href="https://opensource.org/licenses/mit-license.php">
<a href='https://opensource.org/licenses/mit-license.php'>
MIT 协议
</a>
</div>
) : (
<div
className="custom-footer"
className='custom-footer'
dangerouslySetInnerHTML={{ __html: Footer }}
></div>
)}
+22 -19
View File
@@ -2,7 +2,7 @@ import React, { useContext, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { UserContext } from '../context/User';
import { Button, Container, Icon, Menu, Segment } from 'semantic-ui-react';
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
import { API, isAdmin, isMobile, showSuccess } from '../helpers';
import '../index.css';
@@ -34,7 +34,6 @@ const headerButtons = [
const Header = () => {
const [userState, userDispatch] = useContext(UserContext);
let navigate = useNavigate();
let size = isMobile() ? 'large' : '';
const [showSidebar, setShowSidebar] = useState(false);
@@ -80,7 +79,7 @@ const Header = () => {
<>
<Menu
borderless
size={size}
size='large'
style={
showSidebar
? {
@@ -93,17 +92,17 @@ const Header = () => {
}
>
<Container>
<Menu.Item as={Link} to="/">
<Menu.Item as={Link} to='/'>
<img
src="/logo.png"
alt="logo"
src='/logo.png'
alt='logo'
style={{ marginRight: '0.75em' }}
/>
<div style={{ fontSize: '20px' }}>
<b>消息推送服务</b>
</div>
</Menu.Item>
<Menu.Menu position="right">
<Menu.Menu position='right'>
<Menu.Item onClick={toggleSidebar}>
<Icon name={showSidebar ? 'close' : 'sidebar'} />
</Menu.Item>
@@ -149,28 +148,32 @@ const Header = () => {
return (
<>
<Menu borderless size={size} style={{ borderTop: 'none' }}>
<Menu borderless style={{ borderTop: 'none' }}>
<Container>
<Menu.Item as={Link} to="/" className={'hide-on-mobile'}>
<img src="/logo.png" alt="logo" style={{ marginRight: '0.75em' }} />
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
<img src='/logo.png' alt='logo' style={{ marginRight: '0.75em' }} />
<div style={{ fontSize: '20px' }}>
<b>消息推送服务</b>
</div>
</Menu.Item>
{renderButtons(false)}
<Menu.Menu position="right">
<Menu.Menu position='right'>
{userState.user ? (
<Menu.Item
name="注销"
onClick={logout}
className="btn btn-link"
/>
<Dropdown
text={userState.user.username}
pointing
className='link item'
>
<Dropdown.Menu>
<Dropdown.Item onClick={logout}>注销</Dropdown.Item>
</Dropdown.Menu>
</Dropdown>
) : (
<Menu.Item
name="登录"
name='登录'
as={Link}
to="/login"
className="btn btn-link"
to='/login'
className='btn btn-link'
/>
)}
</Menu.Menu>
+2 -2
View File
@@ -114,7 +114,7 @@ const LoginForm = () => {
value={password}
onChange={handleChange}
/>
<Button color='teal' fluid size='large' onClick={handleSubmit}>
<Button color='telegram' fluid size='large' onClick={handleSubmit}>
登录
</Button>
</Segment>
@@ -179,7 +179,7 @@ const LoginForm = () => {
onChange={handleChange}
/>
<Button
color='teal'
color='telegram'
fluid
size='large'
onClick={onSubmitWeChatVerificationCode}
+10 -11
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Modal } from 'semantic-ui-react';
import { Button, Divider, Form, Grid, Header, Modal } from 'semantic-ui-react';
import { API, showError, showSuccess } from '../helpers';
import { marked } from 'marked';
@@ -7,7 +7,7 @@ const OtherSetting = () => {
let [inputs, setInputs] = useState({
Footer: '',
Notice: '',
About: ''
About: '',
});
let originInputs = {};
let [loading, setLoading] = useState(false);
@@ -42,7 +42,7 @@ const OtherSetting = () => {
setLoading(true);
const res = await API.put('/api/option', {
key,
value
value,
});
const { success, message } = res.data;
if (success) {
@@ -76,7 +76,7 @@ const OtherSetting = () => {
const checkUpdate = async () => {
const res = await API.get(
'https://api.github.com/repos/songquanpeng/message-pusher/releases/latest'
'https://api.github.com/repos/songquanpeng/gin-template/releases/latest'
);
const { tag_name, body } = res.data;
if (tag_name === process.env.REACT_APP_VERSION) {
@@ -94,6 +94,7 @@ const OtherSetting = () => {
<Grid columns={1}>
<Grid.Column>
<Form loading={loading}>
<Header as='h3'>通用设置</Header>
<Form.Button onClick={checkUpdate}>检查更新</Form.Button>
<Form.Group widths='equal'>
<Form.TextArea
@@ -106,6 +107,8 @@ const OtherSetting = () => {
/>
</Form.Group>
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
<Divider />
<Header as='h3'>个性化设置</Header>
<Form.Group widths='equal'>
<Form.TextArea
label='关于'
@@ -126,9 +129,7 @@ const OtherSetting = () => {
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={submitFooter}>
设置页脚
</Form.Button>
<Form.Button onClick={submitFooter}>设置页脚</Form.Button>
</Form>
</Grid.Column>
<Modal
@@ -139,15 +140,13 @@ const OtherSetting = () => {
<Modal.Header>新版本{updateData.tag_name}</Modal.Header>
<Modal.Content>
<Modal.Description>
<div
dangerouslySetInnerHTML={{ __html: updateData.content }}
></div>
<div dangerouslySetInnerHTML={{ __html: updateData.content }}></div>
</Modal.Description>
</Modal.Content>
<Modal.Actions>
<Button onClick={() => setShowUpdateModal(false)}>关闭</Button>
<Button
content="详情"
content='详情'
onClick={() => {
setShowUpdateModal(false);
openGitHubRelease();
+2 -2
View File
@@ -33,7 +33,7 @@ const PasswordResetConfirm = () => {
if (success) {
let password = res.data.data;
await copy(password);
showSuccess(`密码已重置并已复制到剪板:${password}`);
showSuccess(`密码已重置并已复制到剪板:${password}`);
} else {
showError(message);
}
@@ -58,7 +58,7 @@ const PasswordResetConfirm = () => {
readOnly
/>
<Button
color='teal'
color='telegram'
fluid
size='large'
onClick={handleSubmit}
+35 -4
View File
@@ -1,6 +1,7 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
import { API, showError, showSuccess } from '../helpers';
import { API, showError, showInfo, showSuccess } from '../helpers';
import Turnstile from 'react-turnstile';
const PasswordResetForm = () => {
const [inputs, setInputs] = useState({
@@ -9,6 +10,20 @@ const PasswordResetForm = () => {
const { email } = inputs;
const [loading, setLoading] = useState(false);
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState('');
useEffect(() => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
if (status.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
}
}, []);
function handleChange(e) {
const { name, value } = e.target;
@@ -17,8 +32,14 @@ const PasswordResetForm = () => {
async function handleSubmit(e) {
if (!email) return;
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
return;
}
setLoading(true);
const res = await API.get(`/api/reset_password?email=${email}`);
const res = await API.get(
`/api/reset_password?email=${email}&turnstile=${turnstileToken}`
);
const { success, message } = res.data;
if (success) {
showSuccess('重置邮件发送成功,请检查邮箱!');
@@ -46,8 +67,18 @@ const PasswordResetForm = () => {
value={email}
onChange={handleChange}
/>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
<Button
color='teal'
color='telegram'
fluid
size='large'
onClick={handleSubmit}
+61 -8
View File
@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Image, Modal } from 'semantic-ui-react';
import { Button, Divider, Form, Header, Image, Modal } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers';
import { API, copy, showError, showInfo, showSuccess } from '../helpers';
import Turnstile from 'react-turnstile';
const PersonalSetting = () => {
const [inputs, setInputs] = useState({
@@ -12,12 +13,20 @@ const PersonalSetting = () => {
const [status, setStatus] = useState({});
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
setStatus(status);
if (status.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
}
}, []);
@@ -25,6 +34,17 @@ const PersonalSetting = () => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const generateToken = async () => {
const res = await API.get('/api/user/token');
const { success, message, data } = res.data;
if (success) {
await copy(data);
showSuccess(`令牌已重置并已复制到剪贴板:${data}`);
} else {
showError(message);
}
};
const bindWeChat = async () => {
if (inputs.wechat_verification_code === '') return;
const res = await API.get(
@@ -47,17 +67,26 @@ const PersonalSetting = () => {
const sendVerificationCode = async () => {
if (inputs.email === '') return;
const res = await API.get(`/api/verification?email=${inputs.email}`);
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
return;
}
setLoading(true);
const res = await API.get(
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
);
const { success, message } = res.data;
if (success) {
showSuccess('验证码发送成功,请检查你的邮箱!');
showSuccess('验证码发送成功,请检查邮箱!');
} else {
showError(message);
}
setLoading(false);
};
const bindEmail = async () => {
if (inputs.email_verification_code === '') return;
setLoading(true);
const res = await API.get(
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
);
@@ -68,13 +97,18 @@ const PersonalSetting = () => {
} else {
showError(message);
}
setLoading(false);
};
return (
<div style={{ lineHeight: '40px' }}>
<Header as='h3'>通用设置</Header>
<Button as={Link} to={`/user/edit/`}>
更新个人信息
</Button>
<Button onClick={generateToken}>生成访问令牌</Button>
<Divider />
<Header as='h3'>账号绑定</Header>
<Button
onClick={() => {
setShowWeChatBindModal(true);
@@ -104,7 +138,7 @@ const PersonalSetting = () => {
value={inputs.wechat_verification_code}
onChange={handleInputChange}
/>
<Button color='teal' fluid size='large' onClick={bindWeChat}>
<Button color='telegram' fluid size='large' onClick={bindWeChat}>
绑定
</Button>
</Form>
@@ -123,7 +157,8 @@ const PersonalSetting = () => {
onClose={() => setShowEmailBindModal(false)}
onOpen={() => setShowEmailBindModal(true)}
open={showEmailBindModal}
size={'mini'}
size={'tiny'}
style={{ maxWidth: '450px' }}
>
<Modal.Header>绑定邮箱地址</Modal.Header>
<Modal.Content>
@@ -136,7 +171,9 @@ const PersonalSetting = () => {
name='email'
type='email'
action={
<Button onClick={sendVerificationCode}>获取验证码</Button>
<Button onClick={sendVerificationCode} disabled={loading}>
获取验证码
</Button>
}
/>
<Form.Input
@@ -146,7 +183,23 @@ const PersonalSetting = () => {
value={inputs.email_verification_code}
onChange={handleInputChange}
/>
<Button color='teal' fluid size='large' onClick={bindEmail}>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
<Button
color='telegram'
fluid
size='large'
onClick={bindEmail}
loading={loading}
>
绑定
</Button>
</Form>
+48 -5
View File
@@ -10,6 +10,7 @@ import {
} from 'semantic-ui-react';
import { Link, useNavigate } from 'react-router-dom';
import { API, showError, showInfo, showSuccess } from '../helpers';
import Turnstile from 'react-turnstile';
const RegisterForm = () => {
const [inputs, setInputs] = useState({
@@ -20,14 +21,21 @@ const RegisterForm = () => {
verification_code: '',
});
const { username, password, password2 } = inputs;
const [showEmailVerification, setShowEmailVerification] = useState(false);
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
const [turnstileToken, setTurnstileToken] = useState('');
const [loading, setLoading] = useState(false);
useEffect(() => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
setShowEmailVerification(status.email_verification);
if (status.turnstile_check) {
setTurnstileEnabled(true);
setTurnstileSiteKey(status.turnstile_site_key);
}
}
});
@@ -49,7 +57,15 @@ const RegisterForm = () => {
return;
}
if (username && password) {
const res = await API.post('/api/user/register', inputs);
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
return;
}
setLoading(true);
const res = await API.post(
`/api/user/register?turnstile=${turnstileToken}`,
inputs
);
const { success, message } = res.data;
if (success) {
navigate('/login');
@@ -57,18 +73,27 @@ const RegisterForm = () => {
} else {
showError(message);
}
setLoading(false);
}
}
const sendVerificationCode = async () => {
if (inputs.email === '') return;
const res = await API.get(`/api/verification?email=${inputs.email}`);
if (turnstileEnabled && turnstileToken === '') {
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
return;
}
setLoading(true);
const res = await API.get(
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
);
const { success, message } = res.data;
if (success) {
showSuccess('验证码发送成功,请检查你的邮箱!');
} else {
showError(message);
}
setLoading(false);
};
return (
@@ -116,7 +141,9 @@ const RegisterForm = () => {
name='email'
type='email'
action={
<Button onClick={sendVerificationCode}>获取验证码</Button>
<Button onClick={sendVerificationCode} disabled={loading}>
获取验证码
</Button>
}
/>
<Form.Input
@@ -131,7 +158,23 @@ const RegisterForm = () => {
) : (
<></>
)}
<Button color='teal' fluid size='large' onClick={handleSubmit}>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
<Button
color='telegram'
fluid
size='large'
onClick={handleSubmit}
loading={loading}
>
注册
</Button>
</Segment>
+154 -45
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { Form, Grid } from 'semantic-ui-react';
import { Divider, Form, Grid, Header } from 'semantic-ui-react';
import { API, showError } from '../helpers';
const SystemSetting = () => {
@@ -20,6 +20,10 @@ const SystemSetting = () => {
WeChatServerAddress: '',
WeChatServerToken: '',
WeChatAccountQRCodeImageURL: '',
TurnstileCheckEnabled: '',
TurnstileSiteKey: '',
TurnstileSecretKey: '',
RegisterEnabled: '',
});
let originInputs = {};
let [loading, setLoading] = useState(false);
@@ -51,6 +55,8 @@ const SystemSetting = () => {
case 'EmailVerificationEnabled':
case 'GitHubOAuthEnabled':
case 'WeChatAuthEnabled':
case 'TurnstileCheckEnabled':
case 'RegisterEnabled':
value = inputs[key] === 'true' ? 'false' : 'true';
break;
default:
@@ -78,7 +84,9 @@ const SystemSetting = () => {
name === 'GitHubClientSecret' ||
name === 'WeChatServerAddress' ||
name === 'WeChatServerToken' ||
name === 'WeChatAccountQRCodeImageURL'
name === 'WeChatAccountQRCodeImageURL' ||
name === 'TurnstileSiteKey' ||
name === 'TurnstileSecretKey'
) {
setInputs((inputs) => ({ ...inputs, [name]: value }));
} else {
@@ -142,125 +150,226 @@ const SystemSetting = () => {
}
};
const submitTurnstile = async () => {
if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) {
await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey);
}
if (
originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey &&
inputs.TurnstileSecretKey !== ''
) {
await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey);
}
};
return (
<Grid columns={1}>
<Grid.Column>
<Form loading={loading}>
<Form.Group widths="equal">
<Header as='h3'>通用设置</Header>
<Form.Group widths='equal'>
<Form.Input
label="服务器地址"
placeholder="例如:https://yourdomain.com(注意没有最后的斜杠)"
label='服务器地址'
placeholder='例如:https://yourdomain.com(注意没有最后的斜杠)'
value={inputs.ServerAddress}
name="ServerAddress"
name='ServerAddress'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Button onClick={submitServerAddress}>
更新服务器地址
</Form.Button>
<Divider />
<Header as='h3'>配置登录注册</Header>
<Form.Group inline>
<Form.Checkbox
checked={inputs.PasswordLoginEnabled === 'true'}
label="允许密码登录"
name="PasswordLoginEnabled"
label='允许通过密码进行登录'
name='PasswordLoginEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.PasswordRegisterEnabled === 'true'}
label="允许通过密码进行注册"
name="PasswordRegisterEnabled"
label='允许通过密码进行注册'
name='PasswordRegisterEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.EmailVerificationEnabled === 'true'}
label="强制邮箱验证"
name="EmailVerificationEnabled"
label='通过密码注册时需要进行邮箱验证'
name='EmailVerificationEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.GitHubOAuthEnabled === 'true'}
label="允许通过 GitHub 账户登录 & 注册"
name="GitHubOAuthEnabled"
label='允许通过 GitHub 账户登录 & 注册'
name='GitHubOAuthEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.WeChatAuthEnabled === 'true'}
label="允许通过微信登录 & 注册"
name="WeChatAuthEnabled"
label='允许通过微信登录 & 注册'
name='WeChatAuthEnabled'
onChange={handleInputChange}
/>
</Form.Group>
<Form.Group inline>
<Form.Checkbox
checked={inputs.RegisterEnabled === 'true'}
label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
name='RegisterEnabled'
onChange={handleInputChange}
/>
<Form.Checkbox
checked={inputs.TurnstileCheckEnabled === 'true'}
label='启用 Turnstile 用户校验'
name='TurnstileCheckEnabled'
onChange={handleInputChange}
/>
</Form.Group>
<Divider />
<Header as='h3'>
配置 SMTP
<Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
</Header>
<Form.Group widths={3}>
<Form.Input
label="SMTP 服务器地址"
name="SMTPServer"
label='SMTP 服务器地址'
name='SMTPServer'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.SMTPServer}
placeholder='例如:smtp.qq.com'
/>
<Form.Input
label="SMTP 账户"
name="SMTPAccount"
label='SMTP 账户'
name='SMTPAccount'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.SMTPAccount}
placeholder='通常是邮箱地址'
/>
<Form.Input
label="SMTP 访问凭证"
name="SMTPToken"
label='SMTP 访问凭证'
name='SMTPToken'
onChange={handleInputChange}
type="password"
autoComplete="off"
type='password'
autoComplete='off'
value={inputs.SMTPToken}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
<Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
<Divider />
<Header as='h3'>
配置 GitHub OAuth App
<Header.Subheader>
用以支持通过 GitHub 进行登录注册
<a href='https://github.com/settings/developers' target='_blank'>
点击此处
</a>
管理你的 GitHub OAuth App
</Header.Subheader>
</Header>
<Form.Group widths={3}>
<Form.Input
label="GitHub Client ID"
name="GitHubClientId"
label='GitHub Client ID'
name='GitHubClientId'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.GitHubClientId}
placeholder='输入你注册的 GitHub OAuth APP 的 ID'
/>
<Form.Input
label="GitHub Client Secret"
name="GitHubClientSecret"
label='GitHub Client Secret'
name='GitHubClientSecret'
onChange={handleInputChange}
type="password"
autoComplete="off"
type='password'
autoComplete='off'
value={inputs.GitHubClientSecret}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
<Form.Button onClick={submitGitHubOAuth}>
保存 GitHub OAuth 设置
</Form.Button>
<Divider />
<Header as='h3'>
配置 WeChat Server
<Header.Subheader>
用以支持通过微信进行登录注册
<a
href='https://github.com/songquanpeng/wechat-server'
target='_blank'
>
点击此处
</a>
了解 WeChat Server
</Header.Subheader>
</Header>
<Form.Group widths={3}>
<Form.Input
label="WeChat Server 服务器地址"
name="WeChatServerAddress"
placeholder="例如:https://yourdomain.com(注意没有最后的斜杠)"
label='WeChat Server 服务器地址'
name='WeChatServerAddress'
placeholder='例如:https://yourdomain.com(注意没有最后的斜杠)'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.WeChatServerAddress}
/>
<Form.Input
label="WeChat Server 访问凭证"
name="WeChatServerToken"
type="password"
label='WeChat Server 访问凭证'
name='WeChatServerToken'
type='password'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.WeChatServerToken}
placeholder='敏感信息不会发送到前端显示'
/>
<Form.Input
label="微信公众号二维码图片链接"
name="WeChatAccountQRCodeImageURL"
label='微信公众号二维码图片链接'
name='WeChatAccountQRCodeImageURL'
onChange={handleInputChange}
autoComplete="off"
autoComplete='off'
value={inputs.WeChatAccountQRCodeImageURL}
placeholder='输入一个图片链接'
/>
</Form.Group>
<Form.Button onClick={submitWeChat}>保存微信登录设置</Form.Button>
<Form.Button onClick={submitWeChat}>
保存 WeChat Server 设置
</Form.Button>
<Divider />
<Header as='h3'>
配置 Turnstile
<Header.Subheader>
用以支持用户校验
<a href='https://dash.cloudflare.com/' target='_blank'>
点击此处
</a>
管理你的 Turnstile Sites推荐选择 Invisible Widget Type
</Header.Subheader>
</Header>
<Form.Group widths={3}>
<Form.Input
label='Turnstile Site Key'
name='TurnstileSiteKey'
onChange={handleInputChange}
autoComplete='off'
value={inputs.TurnstileSiteKey}
placeholder='输入你注册的 Turnstile Site Key'
/>
<Form.Input
label='Turnstile Secret Key'
name='TurnstileSecretKey'
onChange={handleInputChange}
type='password'
autoComplete='off'
value={inputs.TurnstileSecretKey}
placeholder='敏感信息不会发送到前端显示'
/>
</Form.Group>
<Form.Button onClick={submitTurnstile}>
保存 Turnstile 设置
</Form.Button>
</Form>
</Grid.Column>
</Grid>
+75 -31
View File
@@ -3,18 +3,18 @@ import { Button, Form, Label, Pagination, Table } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, showError, showSuccess } from '../helpers';
const itemsPerPage = 10;
import { ITEMS_PER_PAGE } from '../constants';
function renderRole(role) {
switch (role) {
case 1:
return <Label>普通用户</Label>;
case 10:
return <Label color="yellow">管理员</Label>;
return <Label color='yellow'>管理员</Label>;
case 100:
return <Label color="orange">超级管理员</Label>;
return <Label color='orange'>超级管理员</Label>;
default:
return <Label color="red">未知身份</Label>;
return <Label color='red'>未知身份</Label>;
}
}
@@ -25,11 +25,17 @@ const UsersTable = () => {
const [searchKeyword, setSearchKeyword] = useState('');
const [searching, setSearching] = useState(false);
const loadUsers = async () => {
const res = await API.get('/api/user');
const loadUsers = async (startIdx) => {
const res = await API.get(`/api/user/?p=${startIdx}`);
const { success, message, data } = res.data;
if (success) {
setUsers(data);
if (startIdx === 0) {
setUsers(data);
} else {
let newUsers = users;
newUsers.push(...data);
setUsers(newUsers);
}
} else {
showError(message);
}
@@ -37,18 +43,24 @@ const UsersTable = () => {
};
const onPaginationChange = (e, { activePage }) => {
setActivePage(activePage);
(async () => {
if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
// In this case we have to load more data and then append them.
await loadUsers(activePage - 1);
}
setActivePage(activePage);
})();
};
useEffect(() => {
loadUsers()
loadUsers(0)
.then()
.catch((reason) => {
showError(reason);
});
}, []);
const manageUser = (username, action) => {
const manageUser = (username, action, idx) => {
(async () => {
const res = await API.post('/api/user/manage', {
username,
@@ -57,38 +69,62 @@ const UsersTable = () => {
const { success, message } = res.data;
if (success) {
showSuccess('操作成功完成!');
await loadUsers();
let user = res.data.data;
let newUsers = [...users];
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
if (action === 'delete') {
newUsers[realIdx].deleted = true;
} else {
newUsers[realIdx].status = user.status;
newUsers[realIdx].role = user.role;
}
setUsers(newUsers);
} else {
showError(message);
}
})();
};
const renderStatus = (status, id) => {
const renderStatus = (status) => {
switch (status) {
case 1:
return '已激活';
return <Label basic>已激活</Label>;
case 2:
return '已封禁';
return (
<Label basic color='red'>
已封禁
</Label>
);
default:
return '未知状态';
return (
<Label basic color='grey'>
未知状态
</Label>
);
}
};
const searchUsers = async () => {
if (searchKeyword === '') {
// if keyword is blank, load files instead.
await loadUsers(0);
setActivePage(1);
return;
}
setSearching(true);
const res = await API.get(`/api/user/search?keyword=${searchKeyword}`);
const { success, message, data } = res.data;
if (success) {
setUsers(data);
setActivePage(1);
} else {
showError(message);
}
setSearching(false);
};
const handleKeywordChange = async (e, { name, value }) => {
setSearchKeyword(value);
const handleKeywordChange = async (e, { value }) => {
setSearchKeyword(value.trim());
};
const sortUser = (key) => {
@@ -109,10 +145,10 @@ const UsersTable = () => {
<>
<Form onSubmit={searchUsers}>
<Form.Input
icon="search"
icon='search'
fluid
iconPosition="left"
placeholder="搜索用户的 ID,用户名,显示名称,以及邮箱地址 ..."
iconPosition='left'
placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...'
value={searchKeyword}
loading={searching}
onChange={handleKeywordChange}
@@ -168,22 +204,26 @@ const UsersTable = () => {
<Table.Body>
{users
.slice((activePage - 1) * itemsPerPage, activePage * itemsPerPage)
.slice(
(activePage - 1) * ITEMS_PER_PAGE,
activePage * ITEMS_PER_PAGE
)
.map((user, idx) => {
if (user.deleted) return <></>;
return (
<Table.Row key={user.id}>
<Table.Cell>{user.username}</Table.Cell>
<Table.Cell>{user.display_name}</Table.Cell>
<Table.Cell>{user.email ? user.email : '无'}</Table.Cell>
<Table.Cell>{renderRole(user.role)}</Table.Cell>
<Table.Cell>{renderStatus(user.status, user.id)}</Table.Cell>
<Table.Cell>{renderStatus(user.status)}</Table.Cell>
<Table.Cell>
<div>
<Button
size={'small'}
positive
onClick={() => {
manageUser(user.username, 'promote');
manageUser(user.username, 'promote', idx);
}}
>
提升
@@ -192,7 +232,7 @@ const UsersTable = () => {
size={'small'}
color={'yellow'}
onClick={() => {
manageUser(user.username, 'demote');
manageUser(user.username, 'demote', idx);
}}
>
降级
@@ -201,7 +241,7 @@ const UsersTable = () => {
size={'small'}
negative
onClick={() => {
manageUser(user.username, 'delete');
manageUser(user.username, 'delete', idx);
}}
>
删除
@@ -211,7 +251,8 @@ const UsersTable = () => {
onClick={() => {
manageUser(
user.username,
user.status === 1 ? 'disable' : 'enable'
user.status === 1 ? 'disable' : 'enable',
idx
);
}}
>
@@ -233,17 +274,20 @@ const UsersTable = () => {
<Table.Footer>
<Table.Row>
<Table.HeaderCell colSpan="6">
<Button size="small" as={Link} to="/user/add" loading={loading}>
<Table.HeaderCell colSpan='6'>
<Button size='small' as={Link} to='/user/add' loading={loading}>
添加新的用户
</Button>
<Pagination
floated="right"
floated='right'
activePage={activePage}
onPageChange={onPaginationChange}
size="small"
size='small'
siblingRange={1}
totalPages={Math.ceil(users.length / itemsPerPage)}
totalPages={
Math.ceil(users.length / ITEMS_PER_PAGE) +
(users.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
}
/>
</Table.HeaderCell>
</Table.Row>
+1
View File
@@ -0,0 +1 @@
export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
+2 -1
View File
@@ -1,2 +1,3 @@
export * from './toast.constants';
export * from './user.constants';
export * from './user.constants';
export * from './common.constant';
+4
View File
@@ -85,3 +85,7 @@ export function showInfo(message) {
export function showNotice(message) {
toast.info(message, showNoticeOptions);
}
export function openPage(url) {
window.open(url);
}
+8 -6
View File
@@ -26,18 +26,20 @@ const About = () => {
return (
<>
<Segment>
{
about === '' ? <>
{about === '' ? (
<>
<Header as='h3'>关于</Header>
<p>可在设置页面设置关于内容支持 HTML & Markdown</p>
项目仓库地址
<a href="https://github.com/songquanpeng/message-pusher">
<a href='https://github.com/songquanpeng/message-pusher'>
https://github.com/songquanpeng/message-pusher
</a>
</> : <>
<div dangerouslySetInnerHTML={{ __html: about}}></div>
</>
}
) : (
<>
<div dangerouslySetInnerHTML={{ __html: about }}></div>
</>
)}
</Segment>
</>
);
+22 -47
View File
@@ -13,18 +13,9 @@ const EditUser = () => {
password: '',
github_id: '',
wechat_id: '',
email: '',
token: '',
email:''
});
const {
username,
display_name,
password,
github_id,
wechat_id,
email,
token,
} = inputs;
const { username, display_name, password, github_id, wechat_id, email } = inputs;
const handleInputChange = (e, { name, value }) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
@@ -39,9 +30,6 @@ const EditUser = () => {
const { success, message, data } = res.data;
if (success) {
data.password = '';
if (data.token === ' ') {
data.token = '';
}
setInputs(data);
} else {
showError(message);
@@ -70,77 +58,64 @@ const EditUser = () => {
return (
<>
<Segment loading={loading}>
<Header as='h3'>更新用户信息</Header>
<Form autoComplete='off'>
<Header as="h3">更新用户信息</Header>
<Form autoComplete="off">
<Form.Field>
<Form.Input
label='用户名'
name='username'
label="用户名"
name="username"
placeholder={'请输入新的用户名'}
onChange={handleInputChange}
value={username}
autoComplete='off'
autoComplete="off"
/>
</Form.Field>
<Form.Field>
<Form.Input
label='密码'
name='password'
label="密码"
name="password"
type={'password'}
placeholder={'请输入新的密码'}
onChange={handleInputChange}
value={password}
autoComplete='off'
autoComplete="off"
/>
</Form.Field>
<Form.Field>
<Form.Input
label='显示名称'
name='display_name'
label="显示名称"
name="display_name"
placeholder={'请输入新的显示名称'}
onChange={handleInputChange}
value={display_name}
autoComplete='off'
autoComplete="off"
/>
</Form.Field>
<Form.Field>
<Form.Input
label='推送鉴权 Token'
name='token'
placeholder={'请输入新的 Token,留空则将 Token 置空'}
onChange={handleInputChange}
value={token}
autoComplete='off'
/>
</Form.Field>
<Form.Field>
<Form.Input
label='已绑定的 GitHub 账户'
name='github_id'
label="已绑定的 GitHub 账户"
name="github_id"
value={github_id}
autoComplete='off'
autoComplete="off"
readOnly
placeholder={'如需绑定请到个人设置页面进行绑定'}
/>
</Form.Field>
<Form.Field>
<Form.Input
label='已绑定的微信账户'
name='wechat_id'
label="已绑定的微信账户"
name="wechat_id"
value={wechat_id}
autoComplete='off'
autoComplete="off"
readOnly
placeholder={'如需绑定请到个人设置页面进行绑定'}
/>
</Form.Field>
<Form.Field>
<Form.Input
label='已绑定的邮箱账户'
name='email'
label="已绑定的邮箱账户"
name="email"
value={email}
autoComplete='off'
autoComplete="off"
readOnly
placeholder={'如需绑定请到个人设置页面进行绑定'}
/>
</Form.Field>
<Button onClick={submit}>提交</Button>