mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-01 22:46:38 +08:00
feat: add managed domain functionality with matching certificate feature
- Implemented managed domain CRUD operations in the backend with appropriate service and controller logic. - Added matching logic for managed domains to automatically suggest certificates based on domain input. - Enhanced the frontend to support managed domain management, including form handling and displaying match results. - Updated header component to include new managed domain routes. - Added tests for managed domain lifecycle and matching logic.
This commit is contained in:
+232
-223
@@ -1,223 +1,232 @@
|
||||
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';
|
||||
import { PrivateRoute } from './components/PrivateRoute';
|
||||
import RegisterForm from './components/RegisterForm';
|
||||
import LoginForm from './components/LoginForm';
|
||||
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, showNotice } from './helpers';
|
||||
import PasswordResetForm from './components/PasswordResetForm';
|
||||
import GitHubOAuth from './components/GitHubOAuth';
|
||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||
import { UserContext } from './context/User';
|
||||
import { StatusContext } from './context/Status';
|
||||
import File from './pages/File';
|
||||
import ProxyRoute from './pages/ProxyRoute';
|
||||
import ConfigVersion from './pages/ConfigVersion';
|
||||
import Node from './pages/Node';
|
||||
import ApplyLog from './pages/ApplyLog';
|
||||
import TLSCertificate from './pages/TLSCertificate';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const About = lazy(() => import('./pages/About'));
|
||||
|
||||
function App() {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
|
||||
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) {
|
||||
console.log(
|
||||
`GitHub 仓库地址:https://github.com/songquanpeng/gin-template`
|
||||
);
|
||||
localStorage.setItem('status', JSON.stringify(data));
|
||||
statusDispatch({ type: 'set', payload: data });
|
||||
localStorage.setItem('system_name', data.system_name);
|
||||
localStorage.setItem('footer_html', data.footer_html);
|
||||
localStorage.setItem('home_page_link', data.home_page_link);
|
||||
if (
|
||||
data.version !== process.env.REACT_APP_VERSION &&
|
||||
data.version !== 'v0.0.0' &&
|
||||
process.env.REACT_APP_VERSION !== ''
|
||||
) {
|
||||
showNotice(
|
||||
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showError('无法正常连接至服务器!');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
loadStatus().then();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Home />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/proxy-route'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ProxyRoute />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/config-version'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ConfigVersion />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/node'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Node />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/apply-log'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ApplyLog />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/tls-certificate'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<TLSCertificate />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/file'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<File />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<User />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/edit/:id'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/edit'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<AddUser />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/reset'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetConfirm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/login'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/register'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<RegisterForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/reset'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/oauth/github'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<GitHubOAuth />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/setting'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Setting />
|
||||
</Suspense>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/about'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<About />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path='*' element={<NotFound />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
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';
|
||||
import { PrivateRoute } from './components/PrivateRoute';
|
||||
import RegisterForm from './components/RegisterForm';
|
||||
import LoginForm from './components/LoginForm';
|
||||
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, showNotice } from './helpers';
|
||||
import PasswordResetForm from './components/PasswordResetForm';
|
||||
import GitHubOAuth from './components/GitHubOAuth';
|
||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||
import { UserContext } from './context/User';
|
||||
import { StatusContext } from './context/Status';
|
||||
import File from './pages/File';
|
||||
import ProxyRoute from './pages/ProxyRoute';
|
||||
import ConfigVersion from './pages/ConfigVersion';
|
||||
import Node from './pages/Node';
|
||||
import ApplyLog from './pages/ApplyLog';
|
||||
import TLSCertificate from './pages/TLSCertificate';
|
||||
import ManagedDomain from './pages/ManagedDomain';
|
||||
|
||||
const Home = lazy(() => import('./pages/Home'));
|
||||
const About = lazy(() => import('./pages/About'));
|
||||
|
||||
function App() {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
const [statusState, statusDispatch] = useContext(StatusContext);
|
||||
|
||||
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) {
|
||||
console.log(
|
||||
`GitHub 仓库地址:https://github.com/songquanpeng/gin-template`
|
||||
);
|
||||
localStorage.setItem('status', JSON.stringify(data));
|
||||
statusDispatch({ type: 'set', payload: data });
|
||||
localStorage.setItem('system_name', data.system_name);
|
||||
localStorage.setItem('footer_html', data.footer_html);
|
||||
localStorage.setItem('home_page_link', data.home_page_link);
|
||||
if (
|
||||
data.version !== process.env.REACT_APP_VERSION &&
|
||||
data.version !== 'v0.0.0' &&
|
||||
process.env.REACT_APP_VERSION !== ''
|
||||
) {
|
||||
showNotice(
|
||||
`新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
showError('无法正常连接至服务器!');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadUser();
|
||||
loadStatus().then();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route
|
||||
path='/'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Home />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/proxy-route'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ProxyRoute />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/config-version'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ConfigVersion />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/node'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Node />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/apply-log'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ApplyLog />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/managed-domain'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<ManagedDomain />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/tls-certificate'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<TLSCertificate />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/file'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<File />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<User />
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/edit/:id'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/edit'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<EditUser />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/add'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<AddUser />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/user/reset'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetConfirm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/login'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/register'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<RegisterForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/reset'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<PasswordResetForm />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/oauth/github'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<GitHubOAuth />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/setting'
|
||||
element={
|
||||
<PrivateRoute>
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<Setting />
|
||||
</Suspense>
|
||||
</PrivateRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path='/about'
|
||||
element={
|
||||
<Suspense fallback={<Loading></Loading>}>
|
||||
<About />
|
||||
</Suspense>
|
||||
}
|
||||
/>
|
||||
<Route path='*' element={<NotFound />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -1,230 +1,236 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Dropdown,
|
||||
Icon,
|
||||
Menu,
|
||||
Segment,
|
||||
} from 'semantic-ui-react';
|
||||
import { API, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
|
||||
import '../index.css';
|
||||
|
||||
// Header Buttons
|
||||
const headerButtons = [
|
||||
{
|
||||
name: '首页',
|
||||
to: '/',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
name: '规则',
|
||||
to: '/proxy-route',
|
||||
icon: 'sitemap',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '版本',
|
||||
to: '/config-version',
|
||||
icon: 'history',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '节点',
|
||||
to: '/node',
|
||||
icon: 'server',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '记录',
|
||||
to: '/apply-log',
|
||||
icon: 'tasks',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '证书',
|
||||
to: '/tls-certificate',
|
||||
icon: 'lock',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '文件',
|
||||
to: '/file',
|
||||
icon: 'file',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '用户',
|
||||
to: '/user',
|
||||
icon: 'user',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '设置',
|
||||
to: '/setting',
|
||||
icon: 'setting',
|
||||
},
|
||||
{
|
||||
name: '关于',
|
||||
to: '/about',
|
||||
icon: 'info circle',
|
||||
},
|
||||
];
|
||||
|
||||
const Header = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const systemName = getSystemName();
|
||||
|
||||
async function logout() {
|
||||
setShowSidebar(false);
|
||||
await API.get('/api/user/logout');
|
||||
showSuccess('注销成功!');
|
||||
userDispatch({ type: 'logout' });
|
||||
localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setShowSidebar(!showSidebar);
|
||||
};
|
||||
|
||||
const renderButtons = (isMobile) => {
|
||||
return headerButtons.map((button) => {
|
||||
if (button.admin && !isAdmin()) return <></>;
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
navigate(button.to);
|
||||
setShowSidebar(false);
|
||||
}}
|
||||
>
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Menu.Item key={button.name} as={Link} to={button.to}>
|
||||
<Icon name={button.icon} />
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (isMobile()) {
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
borderless
|
||||
size='large'
|
||||
style={
|
||||
showSidebar
|
||||
? {
|
||||
borderBottom: 'none',
|
||||
marginBottom: '0',
|
||||
borderTop: 'none',
|
||||
height: '51px',
|
||||
}
|
||||
: { borderTop: 'none', height: '52px' }
|
||||
}
|
||||
>
|
||||
<Container>
|
||||
<Menu.Item as={Link} to='/'>
|
||||
<img
|
||||
src='/logo.png'
|
||||
alt='logo'
|
||||
style={{ marginRight: '0.75em' }}
|
||||
/>
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
<b>{systemName}</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Menu position='right'>
|
||||
<Menu.Item onClick={toggleSidebar}>
|
||||
<Icon name={showSidebar ? 'close' : 'sidebar'} />
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
</Container>
|
||||
</Menu>
|
||||
{showSidebar ? (
|
||||
<Segment style={{ marginTop: 0, borderTop: '0' }}>
|
||||
<Menu secondary vertical style={{ width: '100%', margin: 0 }}>
|
||||
{renderButtons(true)}
|
||||
<Menu.Item>
|
||||
{userState.user ? (
|
||||
<Button onClick={logout}>注销</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowSidebar(false);
|
||||
navigate('/login');
|
||||
}}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowSidebar(false);
|
||||
navigate('/register');
|
||||
}}
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Segment>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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' }} />
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
<b>{systemName}</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
{renderButtons(false)}
|
||||
<Menu.Menu position='right'>
|
||||
{userState.user ? (
|
||||
<Dropdown
|
||||
text={userState.user.username}
|
||||
pointing
|
||||
className='link item'
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={logout}>注销</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Menu.Item
|
||||
name='登录'
|
||||
as={Link}
|
||||
to='/login'
|
||||
className='btn btn-link'
|
||||
/>
|
||||
)}
|
||||
</Menu.Menu>
|
||||
</Container>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { UserContext } from '../context/User';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Container,
|
||||
Dropdown,
|
||||
Icon,
|
||||
Menu,
|
||||
Segment,
|
||||
} from 'semantic-ui-react';
|
||||
import { API, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
|
||||
import '../index.css';
|
||||
|
||||
// Header Buttons
|
||||
const headerButtons = [
|
||||
{
|
||||
name: '首页',
|
||||
to: '/',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
name: '规则',
|
||||
to: '/proxy-route',
|
||||
icon: 'sitemap',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '版本',
|
||||
to: '/config-version',
|
||||
icon: 'history',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '节点',
|
||||
to: '/node',
|
||||
icon: 'server',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '记录',
|
||||
to: '/apply-log',
|
||||
icon: 'tasks',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '证书',
|
||||
to: '/tls-certificate',
|
||||
icon: 'lock',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '域名',
|
||||
to: '/managed-domain',
|
||||
icon: 'globe',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '文件',
|
||||
to: '/file',
|
||||
icon: 'file',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '用户',
|
||||
to: '/user',
|
||||
icon: 'user',
|
||||
admin: true,
|
||||
},
|
||||
{
|
||||
name: '设置',
|
||||
to: '/setting',
|
||||
icon: 'setting',
|
||||
},
|
||||
{
|
||||
name: '关于',
|
||||
to: '/about',
|
||||
icon: 'info circle',
|
||||
},
|
||||
];
|
||||
|
||||
const Header = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [showSidebar, setShowSidebar] = useState(false);
|
||||
const systemName = getSystemName();
|
||||
|
||||
async function logout() {
|
||||
setShowSidebar(false);
|
||||
await API.get('/api/user/logout');
|
||||
showSuccess('注销成功!');
|
||||
userDispatch({ type: 'logout' });
|
||||
localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
}
|
||||
|
||||
const toggleSidebar = () => {
|
||||
setShowSidebar(!showSidebar);
|
||||
};
|
||||
|
||||
const renderButtons = (isMobile) => {
|
||||
return headerButtons.map((button) => {
|
||||
if (button.admin && !isAdmin()) return <></>;
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Menu.Item
|
||||
onClick={() => {
|
||||
navigate(button.to);
|
||||
setShowSidebar(false);
|
||||
}}
|
||||
>
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Menu.Item key={button.name} as={Link} to={button.to}>
|
||||
<Icon name={button.icon} />
|
||||
{button.name}
|
||||
</Menu.Item>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
if (isMobile()) {
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
borderless
|
||||
size='large'
|
||||
style={
|
||||
showSidebar
|
||||
? {
|
||||
borderBottom: 'none',
|
||||
marginBottom: '0',
|
||||
borderTop: 'none',
|
||||
height: '51px',
|
||||
}
|
||||
: { borderTop: 'none', height: '52px' }
|
||||
}
|
||||
>
|
||||
<Container>
|
||||
<Menu.Item as={Link} to='/'>
|
||||
<img
|
||||
src='/logo.png'
|
||||
alt='logo'
|
||||
style={{ marginRight: '0.75em' }}
|
||||
/>
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
<b>{systemName}</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Menu position='right'>
|
||||
<Menu.Item onClick={toggleSidebar}>
|
||||
<Icon name={showSidebar ? 'close' : 'sidebar'} />
|
||||
</Menu.Item>
|
||||
</Menu.Menu>
|
||||
</Container>
|
||||
</Menu>
|
||||
{showSidebar ? (
|
||||
<Segment style={{ marginTop: 0, borderTop: '0' }}>
|
||||
<Menu secondary vertical style={{ width: '100%', margin: 0 }}>
|
||||
{renderButtons(true)}
|
||||
<Menu.Item>
|
||||
{userState.user ? (
|
||||
<Button onClick={logout}>注销</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowSidebar(false);
|
||||
navigate('/login');
|
||||
}}
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowSidebar(false);
|
||||
navigate('/register');
|
||||
}}
|
||||
>
|
||||
注册
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</Segment>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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' }} />
|
||||
<div style={{ fontSize: '20px' }}>
|
||||
<b>{systemName}</b>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
{renderButtons(false)}
|
||||
<Menu.Menu position='right'>
|
||||
{userState.user ? (
|
||||
<Dropdown
|
||||
text={userState.user.username}
|
||||
pointing
|
||||
className='link item'
|
||||
>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={logout}>注销</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
) : (
|
||||
<Menu.Item
|
||||
name='登录'
|
||||
as={Link}
|
||||
to='/login'
|
||||
className='btn btn-link'
|
||||
/>
|
||||
)}
|
||||
</Menu.Menu>
|
||||
</Container>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Button, Dropdown, Form, Header, Label, Segment, Table } from 'semantic-ui-react';
|
||||
import { API, formatDateTime, showError, showSuccess } from '../../helpers';
|
||||
|
||||
const initialForm = {
|
||||
domain: '',
|
||||
cert_id: '',
|
||||
enabled: true,
|
||||
remark: '',
|
||||
};
|
||||
|
||||
const ManagedDomain = () => {
|
||||
const [domains, setDomains] = useState([]);
|
||||
const [certificates, setCertificates] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [form, setForm] = useState(initialForm);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
|
||||
const loadCertificates = async () => {
|
||||
const res = await API.get('/api/tls-certificates/');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setCertificates(data || []);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const loadDomains = async () => {
|
||||
setLoading(true);
|
||||
const res = await API.get('/api/managed-domains/');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setDomains(data || []);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadCertificates().then();
|
||||
loadDomains().then();
|
||||
}, []);
|
||||
|
||||
const certificateMap = useMemo(() => {
|
||||
const map = new Map();
|
||||
certificates.forEach((certificate) => {
|
||||
map.set(certificate.id, certificate);
|
||||
});
|
||||
return map;
|
||||
}, [certificates]);
|
||||
|
||||
const certificateOptions = certificates.map((certificate) => ({
|
||||
key: certificate.id,
|
||||
value: certificate.id,
|
||||
text: certificate.name,
|
||||
}));
|
||||
|
||||
const resetForm = () => {
|
||||
setForm(initialForm);
|
||||
setEditingId(null);
|
||||
};
|
||||
|
||||
const submitManagedDomain = async () => {
|
||||
setSubmitting(true);
|
||||
const payload = {
|
||||
domain: form.domain.trim(),
|
||||
cert_id: form.cert_id ? Number(form.cert_id) : null,
|
||||
enabled: form.enabled,
|
||||
remark: form.remark.trim(),
|
||||
};
|
||||
const res = editingId
|
||||
? await API.put(`/api/managed-domains/${editingId}`, payload)
|
||||
: await API.post('/api/managed-domains/', payload);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess(editingId ? '域名规则已更新' : '域名规则已创建');
|
||||
resetForm();
|
||||
await loadDomains();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSubmitting(false);
|
||||
};
|
||||
|
||||
const deleteManagedDomain = async (id) => {
|
||||
const res = await API.delete(`/api/managed-domains/${id}`);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('域名规则已删除');
|
||||
await loadDomains();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const beginEdit = (domain) => {
|
||||
setEditingId(domain.id);
|
||||
setForm({
|
||||
domain: domain.domain,
|
||||
cert_id: domain.cert_id || '',
|
||||
enabled: domain.enabled,
|
||||
remark: domain.remark || '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Segment loading={loading}>
|
||||
<Header as='h3'>域名管理</Header>
|
||||
<p className='page-subtitle'>维护精确域名与通配符域名,并为其绑定默认 TLS 证书。</p>
|
||||
|
||||
<Form onSubmit={submitManagedDomain}>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='域名'
|
||||
placeholder='example.com 或 *.example.com'
|
||||
value={form.domain}
|
||||
onChange={(e, { value }) => setForm({ ...form, domain: value })}
|
||||
/>
|
||||
<Form.Field
|
||||
control={Dropdown}
|
||||
selection
|
||||
clearable
|
||||
label='默认证书'
|
||||
placeholder='可选,绑定默认 TLS 证书'
|
||||
options={certificateOptions}
|
||||
value={form.cert_id}
|
||||
onChange={(e, { value }) => setForm({ ...form, cert_id: value || '' })}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Input
|
||||
label='备注'
|
||||
placeholder='可选备注'
|
||||
value={form.remark}
|
||||
onChange={(e, { value }) => setForm({ ...form, remark: value })}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
toggle
|
||||
label='启用规则'
|
||||
checked={form.enabled}
|
||||
onChange={(e, { checked }) => setForm({ ...form, enabled: checked })}
|
||||
style={{ alignSelf: 'flex-end', marginBottom: '1rem' }}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Button primary type='submit' loading={submitting}>
|
||||
{editingId ? '保存修改' : '新增域名'}
|
||||
</Button>
|
||||
{editingId ? (
|
||||
<Button type='button' onClick={resetForm}>
|
||||
取消编辑
|
||||
</Button>
|
||||
) : null}
|
||||
</Form>
|
||||
|
||||
<Table celled stackable className='atsf-table'>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell>域名</Table.HeaderCell>
|
||||
<Table.HeaderCell>绑定证书</Table.HeaderCell>
|
||||
<Table.HeaderCell>状态</Table.HeaderCell>
|
||||
<Table.HeaderCell>备注</Table.HeaderCell>
|
||||
<Table.HeaderCell>更新时间</Table.HeaderCell>
|
||||
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{domains.map((domain) => {
|
||||
const certificate = domain.cert_id ? certificateMap.get(domain.cert_id) : null;
|
||||
return (
|
||||
<Table.Row key={domain.id}>
|
||||
<Table.Cell>{domain.domain}</Table.Cell>
|
||||
<Table.Cell>{certificate ? certificate.name : '未绑定'}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{domain.enabled ? <Label color='green'>启用</Label> : <Label>停用</Label>}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{domain.remark || '无'}</Table.Cell>
|
||||
<Table.Cell>{formatDateTime(domain.updated_at)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Button size='small' onClick={() => beginEdit(domain)}>
|
||||
编辑
|
||||
</Button>
|
||||
<Button size='small' negative onClick={() => deleteManagedDomain(domain.id)}>
|
||||
删除
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</Segment>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManagedDomain;
|
||||
@@ -27,6 +27,8 @@ const ProxyRoute = () => {
|
||||
const [certificates, setCertificates] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const [matching, setMatching] = useState(false);
|
||||
const [matchResult, setMatchResult] = useState(null);
|
||||
const [form, setForm] = useState(initialForm);
|
||||
const [editingId, setEditingId] = useState(null);
|
||||
|
||||
@@ -57,9 +59,45 @@ const ProxyRoute = () => {
|
||||
loadCertificates().then();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!form.enable_https) {
|
||||
setMatchResult(null);
|
||||
return undefined;
|
||||
}
|
||||
const domain = form.domain.trim().toLowerCase();
|
||||
if (!domain) {
|
||||
setMatchResult(null);
|
||||
return undefined;
|
||||
}
|
||||
const timer = setTimeout(async () => {
|
||||
setMatching(true);
|
||||
const res = await API.get('/api/managed-domains/match', {
|
||||
params: { domain },
|
||||
});
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setMatchResult(data || null);
|
||||
if (data?.matched && data?.candidate?.certificate_id) {
|
||||
setForm((current) => {
|
||||
if (!current.enable_https || current.domain.trim().toLowerCase() !== domain || current.cert_id) {
|
||||
return current;
|
||||
}
|
||||
return { ...current, cert_id: data.candidate.certificate_id };
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setMatchResult(null);
|
||||
showError(message);
|
||||
}
|
||||
setMatching(false);
|
||||
}, 400);
|
||||
return () => clearTimeout(timer);
|
||||
}, [form.domain, form.enable_https]);
|
||||
|
||||
const resetForm = () => {
|
||||
setForm(initialForm);
|
||||
setEditingId(null);
|
||||
setMatchResult(null);
|
||||
};
|
||||
|
||||
const submitRoute = async () => {
|
||||
@@ -117,6 +155,7 @@ const ProxyRoute = () => {
|
||||
redirect_http: route.redirect_http || false,
|
||||
remark: route.remark || '',
|
||||
});
|
||||
setMatchResult(null);
|
||||
};
|
||||
|
||||
const certificateOptions = certificates.map((certificate) => ({
|
||||
@@ -180,6 +219,17 @@ const ProxyRoute = () => {
|
||||
onChange={(e, { value }) => setForm({ ...form, cert_id: value || '' })}
|
||||
/>
|
||||
</Form.Group>
|
||||
{form.enable_https ? (
|
||||
<p className='page-subtitle' style={{ marginTop: '-0.5rem' }}>
|
||||
{matching
|
||||
? '正在按域名自动匹配证书...'
|
||||
: matchResult?.matched
|
||||
? `已匹配${matchResult.candidate?.match_type === 'exact' ? '精确' : '通配符'}规则 ${matchResult.candidate?.domain},推荐证书:${matchResult.candidate?.certificate_name}`
|
||||
: form.domain.trim()
|
||||
? '未找到匹配证书,可继续手动选择。'
|
||||
: '输入域名后会自动匹配托管证书。'}
|
||||
</p>
|
||||
) : null}
|
||||
<Form.Group widths='equal'>
|
||||
<Form.Field
|
||||
control={TextArea}
|
||||
|
||||
Reference in New Issue
Block a user