diff --git a/atsf_server/router/web-router.go b/atsf_server/router/web-router.go
index 1be2f194..8b485add 100644
--- a/atsf_server/router/web-router.go
+++ b/atsf_server/router/web-router.go
@@ -5,6 +5,7 @@ import (
"atsflare/controller"
"atsflare/middleware"
"embed"
+ "fmt"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"io/fs"
@@ -22,6 +23,7 @@ func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
router.Use(middleware.GlobalWebRateLimit())
fileDownloadRoute := router.Group("/")
fileDownloadRoute.GET("/upload/:file", middleware.DownloadRateLimit(), controller.DownloadFile)
+ registerLegacyWebRedirects(router)
router.Use(middleware.Cache())
router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
router.NoRoute(func(c *gin.Context) {
@@ -38,6 +40,35 @@ func setWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
})
}
+func registerLegacyWebRedirects(router *gin.Engine) {
+ legacyRoutes := map[string]string{
+ "/proxy-route": "/proxy-routes",
+ "/config-version": "/config-versions",
+ "/node": "/nodes",
+ "/apply-log": "/apply-logs",
+ "/managed-domain": "/managed-domains",
+ "/tls-certificate": "/tls-certificates",
+ "/file": "/files",
+ "/user": "/users",
+ "/user/add": "/users?mode=create",
+ "/user/edit": "/users",
+ "/setting": "/settings",
+ }
+
+ for source, target := range legacyRoutes {
+ router.GET(source, func(target string) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Redirect(http.StatusFound, target)
+ }
+ }(target))
+ }
+
+ router.GET("/user/edit/:id", func(c *gin.Context) {
+ target := fmt.Sprintf("/users?edit=%s", c.Param("id"))
+ c.Redirect(http.StatusFound, target)
+ })
+}
+
func serveExportedPage(c *gin.Context, buildFS fs.FS) bool {
requestPath := strings.Trim(c.Request.URL.Path, "/")
diff --git a/atsf_server/web/app/(dashboard)/page.tsx b/atsf_server/web/app/(dashboard)/page.tsx
index e23f6a89..c020e83b 100644
--- a/atsf_server/web/app/(dashboard)/page.tsx
+++ b/atsf_server/web/app/(dashboard)/page.tsx
@@ -1,14 +1,14 @@
import { DashboardOverview } from '@/features/dashboard/components/dashboard-overview';
import { PageHeader } from '@/components/layout/page-header';
-export default function DashboardPage() {
- return (
-
- );
-}
+export default function DashboardPage() {
+ return (
+
+ );
+}
diff --git a/atsf_server/web/app/config-version/page.tsx b/atsf_server/web/app/config-version/page.tsx
new file mode 100644
index 00000000..786ffb1d
--- /dev/null
+++ b/atsf_server/web/app/config-version/page.tsx
@@ -0,0 +1,5 @@
+import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect';
+
+export default function LegacyConfigVersionPage() {
+ return ;
+}
diff --git a/atsf_server/web/app/file/page.tsx b/atsf_server/web/app/file/page.tsx
new file mode 100644
index 00000000..b7e741a5
--- /dev/null
+++ b/atsf_server/web/app/file/page.tsx
@@ -0,0 +1,5 @@
+import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect';
+
+export default function LegacyFilePage() {
+ return ;
+}
diff --git a/atsf_server/web/app/node/page.tsx b/atsf_server/web/app/node/page.tsx
new file mode 100644
index 00000000..a2743058
--- /dev/null
+++ b/atsf_server/web/app/node/page.tsx
@@ -0,0 +1,5 @@
+import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect';
+
+export default function LegacyNodePage() {
+ return ;
+}
diff --git a/atsf_server/web/app/proxy-route/page.tsx b/atsf_server/web/app/proxy-route/page.tsx
new file mode 100644
index 00000000..6293ea5a
--- /dev/null
+++ b/atsf_server/web/app/proxy-route/page.tsx
@@ -0,0 +1,5 @@
+import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect';
+
+export default function LegacyProxyRoutePage() {
+ return ;
+}
diff --git a/atsf_server/web/app/setting/page.tsx b/atsf_server/web/app/setting/page.tsx
new file mode 100644
index 00000000..e4ada263
--- /dev/null
+++ b/atsf_server/web/app/setting/page.tsx
@@ -0,0 +1,5 @@
+import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect';
+
+export default function LegacySettingPage() {
+ return ;
+}
diff --git a/atsf_server/web/app/user/add/page.tsx b/atsf_server/web/app/user/add/page.tsx
new file mode 100644
index 00000000..fccf7ae8
--- /dev/null
+++ b/atsf_server/web/app/user/add/page.tsx
@@ -0,0 +1,5 @@
+import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect';
+
+export default function LegacyUserAddPage() {
+ return ;
+}
diff --git a/atsf_server/web/app/user/edit/page.tsx b/atsf_server/web/app/user/edit/page.tsx
new file mode 100644
index 00000000..3d9c9762
--- /dev/null
+++ b/atsf_server/web/app/user/edit/page.tsx
@@ -0,0 +1,5 @@
+import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect';
+
+export default function LegacyUserEditPage() {
+ return ;
+}
diff --git a/atsf_server/web/app/user/page.tsx b/atsf_server/web/app/user/page.tsx
new file mode 100644
index 00000000..c04350c3
--- /dev/null
+++ b/atsf_server/web/app/user/page.tsx
@@ -0,0 +1,5 @@
+import { LegacyRouteRedirect } from '@/features/shared/components/legacy-route-redirect';
+
+export default function LegacyUserPage() {
+ return ;
+}
diff --git a/atsf_server/web/components/layout/dashboard-sidebar.tsx b/atsf_server/web/components/layout/dashboard-sidebar.tsx
index 31da3041..f1247da8 100644
--- a/atsf_server/web/components/layout/dashboard-sidebar.tsx
+++ b/atsf_server/web/components/layout/dashboard-sidebar.tsx
@@ -64,11 +64,11 @@ export function DashboardSidebar() {
})}
- {!isSidebarCollapsed ? (
-
- 阶段 4 已接入设置、用户、文件等边缘模块,旧前端仅保留少量兼容兜底价值。
-
- ) : null}
+ {!isSidebarCollapsed ? (
+
+ 新版管理端已覆盖认证、核心链路和边缘模块,旧版前端代码已完成移除。
+
+ ) : null}
);
diff --git a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx
index 9a0c08b6..dfa85a0c 100644
--- a/atsf_server/web/features/dashboard/components/dashboard-overview.tsx
+++ b/atsf_server/web/features/dashboard/components/dashboard-overview.tsx
@@ -19,13 +19,13 @@ const readinessItems = [
},
];
-export function DashboardOverview() {
- return (
-
-
}
+export function DashboardOverview() {
+ return (
+
+
}
>
{readinessItems.map((item) => (
@@ -60,14 +60,14 @@ export function DashboardOverview() {
-
-
- - 1. 对照旧前端完成设置、用户、文件与关于页的联调回归。
- - 2. 补齐关键页面的测试覆盖与构建验收。
- - 3. 评估默认入口切换与旧前端回滚预案。
-
-
-
-
- );
-}
+
+
+ - 1. 对照后端接口完成设置、节点、证书和用户链路联调。
+ - 2. 补齐关键页面的测试覆盖与构建验收。
+ - 3. 清理迁移期文档和发布说明中的旧前端表述。
+
+
+
+
+ );
+}
diff --git a/atsf_server/web/features/settings/components/settings-page.tsx b/atsf_server/web/features/settings/components/settings-page.tsx
index 7d43c414..37adec8a 100644
--- a/atsf_server/web/features/settings/components/settings-page.tsx
+++ b/atsf_server/web/features/settings/components/settings-page.tsx
@@ -4,11 +4,12 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { marked } from 'marked';
import { useEffect, useMemo, useState } from 'react';
-import { EmptyState } from '@/components/feedback/empty-state';
-import { ErrorState } from '@/components/feedback/error-state';
-import { InlineMessage } from '@/components/feedback/inline-message';
-import { LoadingState } from '@/components/feedback/loading-state';
-import { useAuth } from '@/components/providers/auth-provider';
+import { EmptyState } from '@/components/feedback/empty-state';
+import { ErrorState } from '@/components/feedback/error-state';
+import { InlineMessage } from '@/components/feedback/inline-message';
+import { LoadingState } from '@/components/feedback/loading-state';
+import { TurnstileWidget } from '@/components/forms/turnstile-widget';
+import { useAuth } from '@/components/providers/auth-provider';
import { PageHeader } from '@/components/layout/page-header';
import { AppCard } from '@/components/ui/app-card';
import { StatusBadge } from '@/components/ui/status-badge';
@@ -156,10 +157,11 @@ export function SettingsPage() {
const [operationFields, setOperationFields] = useState(defaultOperationFields);
const [otherFields, setOtherFields] = useState(defaultOtherFields);
const [accessToken, setAccessToken] = useState('');
- const [wechatCode, setWeChatCode] = useState('');
- const [emailAddress, setEmailAddress] = useState('');
- const [emailCode, setEmailCode] = useState('');
- const [latestRelease, setLatestRelease] = useState(null);
+ const [wechatCode, setWeChatCode] = useState('');
+ const [emailAddress, setEmailAddress] = useState('');
+ const [emailCode, setEmailCode] = useState('');
+ const [emailTurnstileToken, setEmailTurnstileToken] = useState('');
+ const [latestRelease, setLatestRelease] = useState(null);
const isRoot = (user?.role ?? 0) >= 100;
@@ -355,17 +357,22 @@ export function SettingsPage() {
});
};
- const handleEmailVerification = () => {
- if (!emailAddress.trim()) {
- setFeedback({ tone: 'danger', message: '请输入要绑定的邮箱地址。' });
- return;
- }
-
- void runBusyAction('email-send', async () => {
- await sendEmailVerification(emailAddress.trim());
- setFeedback({ tone: 'success', message: '验证码已发送,请检查邮箱。' });
- });
- };
+ const handleEmailVerification = () => {
+ if (!emailAddress.trim()) {
+ setFeedback({ tone: 'danger', message: '请输入要绑定的邮箱地址。' });
+ return;
+ }
+
+ if (publicStatusQuery.data?.turnstile_check && !emailTurnstileToken) {
+ setFeedback({ tone: 'info', message: '请先完成人机验证。' });
+ return;
+ }
+
+ void runBusyAction('email-send', async () => {
+ await sendEmailVerification(emailAddress.trim(), emailTurnstileToken || undefined);
+ setFeedback({ tone: 'success', message: '验证码已发送,请检查邮箱。' });
+ });
+ };
const handleBindEmail = () => {
if (!emailAddress.trim() || !emailCode.trim()) {
@@ -591,38 +598,46 @@ export function SettingsPage() {
当前状态:{profile.email ? `已绑定 ${profile.email}` : '未绑定'}
- {publicStatus.turnstile_check ? (
-
- ) : (
-
-
- setEmailAddress(event.target.value)}
- placeholder='请输入邮箱地址'
- />
-
-
- setEmailCode(event.target.value)}
- placeholder='请输入邮箱验证码'
- />
-
-
-
- {busyKey === 'email-send' ? '发送中...' : '发送验证码'}
-
-
- {busyKey === 'email-bind' ? '绑定中...' : '绑定邮箱'}
-
-
-
- )}
-
+
+
+ setEmailAddress(event.target.value)}
+ placeholder='请输入邮箱地址'
+ />
+
+
+ setEmailCode(event.target.value)}
+ placeholder='请输入邮箱验证码'
+ />
+
+ {publicStatus.turnstile_check ? (
+ publicStatus.turnstile_site_key ? (
+
setEmailTurnstileToken(token)}
+ onExpire={() => setEmailTurnstileToken('')}
+ onError={() => setEmailTurnstileToken('')}
+ />
+ ) : (
+
+ )
+ ) : null}
+
+
+ {busyKey === 'email-send' ? '发送中...' : '发送验证码'}
+
+
+ {busyKey === 'email-bind' ? '绑定中...' : '绑定邮箱'}
+
+
+
+
diff --git a/atsf_server/web/features/users/components/users-page.tsx b/atsf_server/web/features/users/components/users-page.tsx
index b19ccaac..79577cbd 100644
--- a/atsf_server/web/features/users/components/users-page.tsx
+++ b/atsf_server/web/features/users/components/users-page.tsx
@@ -1,10 +1,11 @@
-'use client';
-
-import { zodResolver } from '@hookform/resolvers/zod';
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { useEffect, useMemo, useState } from 'react';
-import { useForm } from 'react-hook-form';
-import { z } from 'zod';
+'use client';
+
+import { zodResolver } from '@hookform/resolvers/zod';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
+import { useSearchParams } from 'next/navigation';
+import { useEffect, useMemo, useState } from 'react';
+import { useForm } from 'react-hook-form';
+import { z } from 'zod';
import { EmptyState } from '@/components/feedback/empty-state';
import { ErrorState } from '@/components/feedback/error-state';
@@ -92,10 +93,11 @@ function toPayload(values: UserFormValues): UserMutationPayload {
};
}
-export function UsersPage() {
- const queryClient = useQueryClient();
- const { user: currentUser } = useAuth();
- const [page, setPage] = useState(0);
+export function UsersPage() {
+ const queryClient = useQueryClient();
+ const searchParams = useSearchParams();
+ const { user: currentUser } = useAuth();
+ const [page, setPage] = useState(0);
const [searchInput, setSearchInput] = useState('');
const [searchKeyword, setSearchKeyword] = useState('');
const [editingUserId, setEditingUserId] = useState(null);
@@ -127,15 +129,35 @@ export function UsersPage() {
enabled: isAdmin && editingUserId !== null,
});
- useEffect(() => {
- if (editingUserQuery.data) {
- form.reset({
+ useEffect(() => {
+ if (editingUserQuery.data) {
+ form.reset({
username: editingUserQuery.data.username,
display_name: editingUserQuery.data.display_name || '',
password: '',
});
- }
- }, [editingUserQuery.data, form]);
+ }
+ }, [editingUserQuery.data, form]);
+
+ useEffect(() => {
+ const editParam = searchParams?.get('edit');
+ const modeParam = searchParams?.get('mode');
+
+ if (editParam) {
+ const parsedUserId = Number.parseInt(editParam, 10);
+ if (!Number.isNaN(parsedUserId) && parsedUserId > 0) {
+ setFeedback(null);
+ setEditingUserId(parsedUserId);
+ }
+ return;
+ }
+
+ if (modeParam === 'create') {
+ setFeedback(null);
+ setEditingUserId(null);
+ form.reset(defaultValues);
+ }
+ }, [form, searchParams]);
const saveMutation = useMutation({
mutationFn: async (values: UserFormValues) => {
diff --git a/atsf_server/web/next-env.d.ts b/atsf_server/web/next-env.d.ts
index dab0f084..76faf1f3 100644
--- a/atsf_server/web/next-env.d.ts
+++ b/atsf_server/web/next-env.d.ts
@@ -1,6 +1,5 @@
///
///
-///
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/atsf_server/web/src/App.js b/atsf_server/web/src/App.js
deleted file mode 100644
index 88ba6272..00000000
--- a/atsf_server/web/src/App.js
+++ /dev/null
@@ -1,240 +0,0 @@
-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,
- getBuildVersion,
- hasBuildVersion,
- normalizeVersion,
- 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/Rain-kl/ATSFlare`);
- 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);
- const currentBuildVersion = getBuildVersion();
- const latestServerVersion = normalizeVersion(data.version);
- if (
- hasBuildVersion() &&
- latestServerVersion !== '' &&
- latestServerVersion !== '0.0.0' &&
- latestServerVersion !== currentBuildVersion
- ) {
- showNotice(
- `新版本可用:${data.version},请使用快捷键 Shift + F5 刷新页面`
- );
- }
- } else {
- showError('无法正常连接至服务器!');
- }
- };
-
- useEffect(() => {
- loadUser();
- loadStatus().then();
- }, []);
-
- return (
-
- }>
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
-
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
- }>
-
-
- }
- />
-
- }>
-
-
-
- }
- />
- }>
-
-
- }
- />
- } />
-
- );
-}
-
-export default App;
diff --git a/atsf_server/web/src/components/FilesTable.js b/atsf_server/web/src/components/FilesTable.js
deleted file mode 100644
index 79b290bb..00000000
--- a/atsf_server/web/src/components/FilesTable.js
+++ /dev/null
@@ -1,303 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button,
- Form,
- Header,
- Icon,
- Pagination,
- Popup,
- Progress,
- Segment,
- Table,
-} from 'semantic-ui-react';
-import { API, copy, showError, showSuccess } from '../helpers';
-import { useDropzone } from 'react-dropzone';
-import { ITEMS_PER_PAGE } from '../constants';
-
-const FilesTable = () => {
- const [files, setFiles] = useState([]);
- const [loading, setLoading] = useState(true);
- const [activePage, setActivePage] = useState(1);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [searching, setSearching] = useState(false);
- const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
- const [uploading, setUploading] = useState(false);
- const [uploadProgress, setUploadProgress] = useState('0');
-
- const loadFiles = async (startIdx) => {
- const res = await API.get(`/api/file/?p=${startIdx}`);
- const { success, message, data } = res.data;
- if (success) {
- if (startIdx === 0) {
- setFiles(data);
- } else {
- let newFiles = files;
- newFiles.push(...data);
- setFiles(newFiles);
- }
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- const onPaginationChange = (e, { activePage }) => {
- (async () => {
- if (activePage === Math.ceil(files.length / ITEMS_PER_PAGE) + 1) {
- // In this case we have to load more data and then append them.
- await loadFiles(activePage - 1);
- }
- setActivePage(activePage);
- })();
- };
-
- useEffect(() => {
- loadFiles(0)
- .then()
- .catch((reason) => {
- showError(reason);
- });
- }, []);
-
- const downloadFile = (link, filename) => {
- let linkElement = document.createElement('a');
- linkElement.download = filename;
- linkElement.href = '/upload/' + link;
- linkElement.click();
- };
-
- const copyLink = (link) => {
- let url = window.location.origin + '/upload/' + link;
- copy(url).then();
- showSuccess('链接已复制到剪贴板');
- };
-
- const deleteFile = async (id, idx) => {
- const res = await API.delete(`/api/file/${id}`);
- const { success, message } = res.data;
- if (success) {
- let newFiles = [...files];
- let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
- newFiles[realIdx].deleted = true;
- // newFiles.splice(idx, 1);
- setFiles(newFiles);
- showSuccess('文件已删除!');
- } else {
- showError(message);
- }
- };
-
- const searchFiles = async () => {
- if (searchKeyword === '') {
- // if keyword is blank, load files instead.
- await loadFiles(0);
- setActivePage(1);
- return;
- }
- setSearching(true);
- const res = await API.get(`/api/file/search?keyword=${searchKeyword}`);
- const { success, message, data } = res.data;
- if (success) {
- setFiles(data);
- setActivePage(1);
- } else {
- showError(message);
- }
- setSearching(false);
- };
-
- const handleKeywordChange = async (e, { value }) => {
- setSearchKeyword(value.trim());
- };
-
- const sortFile = (key) => {
- if (files.length === 0) return;
- setLoading(true);
- let sortedUsers = [...files];
- sortedUsers.sort((a, b) => {
- return ('' + a[key]).localeCompare(b[key]);
- });
- if (sortedUsers[0].id === files[0].id) {
- sortedUsers.reverse();
- }
- setFiles(sortedUsers);
- setLoading(false);
- };
-
- const uploadFiles = async () => {
- if (acceptedFiles.length === 0) return;
- setUploading(true);
- let formData = new FormData();
- for (let i = 0; i < acceptedFiles.length; i++) {
- formData.append('file', acceptedFiles[i]);
- }
- const res = await API.post(`/api/file`, formData, {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
- onUploadProgress: (e) => {
- let uploadProgress = ((e.loaded / e.total) * 100).toFixed(2);
- setUploadProgress(uploadProgress);
- },
- });
- const { success, message } = res.data;
- if (success) {
- showSuccess(`${acceptedFiles.length} 个文件上传成功!`);
- } else {
- showError(message);
- }
- setUploading(false);
- setUploadProgress('0');
- setSearchKeyword('');
- loadFiles(0).then();
- setActivePage(1);
- };
-
- useEffect(() => {
- uploadFiles().then();
- }, [acceptedFiles]);
-
- return (
- <>
-
-
-
- {uploading ? (
-
- ) : (
- <>>
- )}
-
-
-
-
-
-
- {
- sortFile('filename');
- }}
- >
- 文件名
-
- {
- sortFile('uploader_id');
- }}
- >
- 上传者
-
- {
- sortFile('email');
- }}
- >
- 上传时间
-
- 操作
-
-
-
-
- {files
- .slice(
- (activePage - 1) * ITEMS_PER_PAGE,
- activePage * ITEMS_PER_PAGE
- )
- .map((file, idx) => {
- if (file.deleted) return <>>;
- return (
-
-
-
- {file.filename}
-
-
- {file.uploader}}
- />
- {file.upload_time}
-
-
-
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export default FilesTable;
diff --git a/atsf_server/web/src/components/Footer.js b/atsf_server/web/src/components/Footer.js
deleted file mode 100644
index a744947a..00000000
--- a/atsf_server/web/src/components/Footer.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import React from 'react';
-
-import { Container, Segment } from 'semantic-ui-react';
-import { getFooterHTML, getSystemName } from '../helpers';
-
-const Footer = () => {
- const systemName = getSystemName();
- const footer = getFooterHTML();
-
- return (
-
-
- {footer ? (
-
- ) : (
-
- )}
-
-
- );
-};
-
-export default Footer;
diff --git a/atsf_server/web/src/components/GitHubOAuth.js b/atsf_server/web/src/components/GitHubOAuth.js
deleted file mode 100644
index 147d4d30..00000000
--- a/atsf_server/web/src/components/GitHubOAuth.js
+++ /dev/null
@@ -1,57 +0,0 @@
-import React, { useContext, useEffect, useState } from 'react';
-import { Dimmer, Loader, Segment } from 'semantic-ui-react';
-import { useNavigate, useSearchParams } from 'react-router-dom';
-import { API, showError, showSuccess } from '../helpers';
-import { UserContext } from '../context/User';
-
-const GitHubOAuth = () => {
- const [searchParams, setSearchParams] = useSearchParams();
-
- const [userState, userDispatch] = useContext(UserContext);
- const [prompt, setPrompt] = useState('处理中...');
- const [processing, setProcessing] = useState(true);
-
- let navigate = useNavigate();
-
- const sendCode = async (code, count) => {
- const res = await API.get(`/api/oauth/github?code=${code}`);
- const { success, message, data } = res.data;
- if (success) {
- if (message === 'bind') {
- showSuccess('绑定成功!');
- navigate('/setting');
- } else {
- userDispatch({ type: 'login', payload: data });
- localStorage.setItem('user', JSON.stringify(data));
- showSuccess('登录成功!');
- navigate('/');
- }
- } else {
- showError(message);
- if (count === 0) {
- setPrompt(`操作失败,重定向至登录界面中...`);
- navigate('/setting'); // in case this is failed to bind GitHub
- return;
- }
- count++;
- setPrompt(`出现错误,第 ${count} 次重试中...`);
- await new Promise((resolve) => setTimeout(resolve, count * 2000));
- await sendCode(code, count);
- }
- };
-
- useEffect(() => {
- let code = searchParams.get('code');
- sendCode(code, 0).then();
- }, []);
-
- return (
-
-
- {prompt}
-
-
- );
-};
-
-export default GitHubOAuth;
diff --git a/atsf_server/web/src/components/Header.js b/atsf_server/web/src/components/Header.js
deleted file mode 100644
index dabe34a2..00000000
--- a/atsf_server/web/src/components/Header.js
+++ /dev/null
@@ -1,230 +0,0 @@
-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: '/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 (
- {
- navigate(button.to);
- setShowSidebar(false);
- }}
- >
- {button.name}
-
- );
- }
- return (
-
-
- {button.name}
-
- );
- });
- };
-
- if (isMobile()) {
- return (
- <>
-
- {showSidebar ? (
-
-
-
- ) : (
- <>>
- )}
- >
- );
- }
-
- return (
- <>
-
- >
- );
-};
-
-export default Header;
diff --git a/atsf_server/web/src/components/Loading.js b/atsf_server/web/src/components/Loading.js
deleted file mode 100644
index 1210a56f..00000000
--- a/atsf_server/web/src/components/Loading.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-import { Segment, Dimmer, Loader } from 'semantic-ui-react';
-
-const Loading = ({ prompt: name = 'page' }) => {
- return (
-
-
- 加载{name}中...
-
-
- );
-};
-
-export default Loading;
diff --git a/atsf_server/web/src/components/LoginForm.js b/atsf_server/web/src/components/LoginForm.js
deleted file mode 100644
index 27351d60..00000000
--- a/atsf_server/web/src/components/LoginForm.js
+++ /dev/null
@@ -1,202 +0,0 @@
-import React, { useContext, useEffect, useState } from 'react';
-import {
- Button,
- Divider,
- Form,
- Grid,
- Header,
- Image,
- Message,
- Modal,
- Segment,
-} from 'semantic-ui-react';
-import { Link, useNavigate, useSearchParams } from 'react-router-dom';
-import { UserContext } from '../context/User';
-import { API, showError, showSuccess } from '../helpers';
-
-const LoginForm = () => {
- const [inputs, setInputs] = useState({
- username: '',
- password: '',
- wechat_verification_code: '',
- });
- const [searchParams, setSearchParams] = useSearchParams();
- const [submitted, setSubmitted] = useState(false);
- const { username, password } = inputs;
- const [userState, userDispatch] = useContext(UserContext);
- let navigate = useNavigate();
-
- const [status, setStatus] = useState({});
-
- useEffect(() => {
- if (searchParams.get("expired")) {
- showError('未登录或登录已过期,请重新登录!');
- }
- let status = localStorage.getItem('status');
- if (status) {
- status = JSON.parse(status);
- setStatus(status);
- }
- }, []);
-
- const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false);
-
- const onGitHubOAuthClicked = () => {
- window.open(
- `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
- );
- };
-
- const onWeChatLoginClicked = () => {
- setShowWeChatLoginModal(true);
- };
-
- const onSubmitWeChatVerificationCode = async () => {
- const res = await API.get(
- `/api/oauth/wechat?code=${inputs.wechat_verification_code}`
- );
- const { success, message, data } = res.data;
- if (success) {
- userDispatch({ type: 'login', payload: data });
- localStorage.setItem('user', JSON.stringify(data));
- navigate('/');
- showSuccess('登录成功!');
- setShowWeChatLoginModal(false);
- } else {
- showError(message);
- }
- };
-
- function handleChange(e) {
- const { name, value } = e.target;
- setInputs((inputs) => ({ ...inputs, [name]: value }));
- }
-
- async function handleSubmit(e) {
- setSubmitted(true);
- if (username && password) {
- const res = await API.post('/api/user/login', {
- username,
- password,
- });
- const { success, message, data } = res.data;
- if (success) {
- userDispatch({ type: 'login', payload: data });
- localStorage.setItem('user', JSON.stringify(data));
- navigate('/');
- showSuccess('登录成功!');
- } else {
- showError(message);
- }
- }
- }
-
- return (
-
-
-
-
-
- 忘记密码?
-
- 点击重置
-
- ; 没有账户?
-
- 点击注册
-
-
- {status.github_oauth || status.wechat_login ? (
- <>
- Or
- {status.github_oauth ? (
-
- ) : (
- <>>
- )}
- {status.wechat_login ? (
-
- ) : (
- <>>
- )}
- >
- ) : (
- <>>
- )}
- setShowWeChatLoginModal(false)}
- onOpen={() => setShowWeChatLoginModal(true)}
- open={showWeChatLoginModal}
- size={'mini'}
- >
-
-
-
-
-
- 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default LoginForm;
diff --git a/atsf_server/web/src/components/OperationSetting.js b/atsf_server/web/src/components/OperationSetting.js
deleted file mode 100644
index 1fa061c4..00000000
--- a/atsf_server/web/src/components/OperationSetting.js
+++ /dev/null
@@ -1,270 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button,
- Divider,
- Form,
- Grid,
- Header,
- Icon,
- Label,
- Message,
- Segment,
-} from 'semantic-ui-react';
-import { API, copy, showError, showSuccess } from '../helpers';
-
-const OperationSetting = () => {
- const [inputs, setInputs] = useState({
- AgentHeartbeatInterval: '30000',
- AgentSyncInterval: '30000',
- NodeOfflineThreshold: '120000',
- AgentUpdateRepo: 'Rain-kl/ATSFlare',
- ServerAddress: '',
- AgentDiscoveryToken: '',
- });
- const [loading, setLoading] = useState(false);
-
- const getOptions = async () => {
- const res = await API.get('/api/option/');
- const { success, message, data } = res.data;
- if (success) {
- let newInputs = {};
- data.forEach((item) => {
- if (item.key in inputs) {
- newInputs[item.key] = item.value;
- }
- });
- setInputs((prev) => ({ ...prev, ...newInputs }));
- } else {
- showError(message);
- }
- };
-
- useEffect(() => {
- getOptions();
- getDiscoveryToken();
- }, []);
-
- const getDiscoveryToken = async () => {
- try {
- const res = await API.get('/api/nodes/bootstrap-token');
- const { success, data } = res.data;
- if (success && data) {
- setInputs((prev) => ({
- ...prev,
- AgentDiscoveryToken: data.discovery_token,
- }));
- }
- } catch (e) {
- // ignore
- }
- };
-
- const updateOption = async (key, value) => {
- setLoading(true);
- const res = await API.put('/api/option', { key, value });
- const { success, message } = res.data;
- if (success) {
- showSuccess('设置已保存');
- setInputs((prev) => ({ ...prev, [key]: value }));
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- const handleInputChange = (e, { name, value }) => {
- setInputs((prev) => ({ ...prev, [name]: value }));
- };
-
- const submitAgentIntervals = async () => {
- const hb = parseInt(inputs.AgentHeartbeatInterval, 10);
- const sync = parseInt(inputs.AgentSyncInterval, 10);
- const offline = parseInt(inputs.NodeOfflineThreshold, 10);
- if (isNaN(hb) || hb < 5000) {
- showError('心跳间隔不能小于 5000 毫秒');
- return;
- }
- if (isNaN(sync) || sync < 5000) {
- showError('同步间隔不能小于 5000 毫秒');
- return;
- }
- if (isNaN(offline) || offline < 10000) {
- showError('离线阈值不能小于 10000 毫秒');
- return;
- }
- await updateOption('AgentHeartbeatInterval', String(hb));
- await updateOption('AgentSyncInterval', String(sync));
- await updateOption('NodeOfflineThreshold', String(offline));
- };
-
- const submitUpdateRepo = async () => {
- await updateOption('AgentUpdateRepo', inputs.AgentUpdateRepo);
- };
-
- const rotateDiscoveryToken = async () => {
- setLoading(true);
- try {
- const res = await API.post('/api/nodes/bootstrap-token/rotate');
- const { success, message, data } = res.data;
- if (success) {
- showSuccess('Discovery Token 已重新生成');
- setInputs((prev) => ({
- ...prev,
- AgentDiscoveryToken: data.discovery_token,
- }));
- } else {
- showError(message);
- }
- } catch (e) {
- showError('操作失败');
- }
- setLoading(false);
- };
-
- const formatMs = (ms) => {
- const val = parseInt(ms, 10);
- if (isNaN(val)) return ms;
- if (val >= 60000) return `${val / 60000} 分钟`;
- return `${val / 1000} 秒`;
- };
-
- const serverAddr = inputs.ServerAddress || window.location.origin;
- const curlCommand = inputs.AgentDiscoveryToken
- ? `curl -fsSL https://raw.githubusercontent.com/Rain-kl/ATSFlare/main/scripts/install-agent.sh | bash -s -- --server-url ${serverAddr} --discovery-token ${inputs.AgentDiscoveryToken}`
- : '';
- const openSwaggerDoc = () => {
- window.open('/swagger/index.html', '_blank', 'noopener,noreferrer');
- };
-
- return (
-
-
-
-
-
-
-
-
- 保存运行参数
-
-
-
-
-
-
- 自动更新开关和手动更新由节点页单独控制,这里只配置 Agent
- 更新仓库。
-
-
-
-
-
-
- 保存更新设置
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {curlCommand && (
- <>
-
-
-
- {curlCommand}
-
-
-
- >
- )}
-
-
-
- );
-};
-
-export default OperationSetting;
diff --git a/atsf_server/web/src/components/OtherSetting.js b/atsf_server/web/src/components/OtherSetting.js
deleted file mode 100644
index 3ccf3994..00000000
--- a/atsf_server/web/src/components/OtherSetting.js
+++ /dev/null
@@ -1,202 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Divider, Form, Grid, Header, Modal } from 'semantic-ui-react';
-import {
- API,
- getBuildVersion,
- hasBuildVersion,
- normalizeVersion,
- showError,
- showSuccess,
-} from '../helpers';
-import { marked } from 'marked';
-
-const OtherSetting = () => {
- let [inputs, setInputs] = useState({
- Footer: '',
- Notice: '',
- About: '',
- SystemName: '',
- HomePageLink: '',
- });
- let [loading, setLoading] = useState(false);
- const [showUpdateModal, setShowUpdateModal] = useState(false);
- const [updateData, setUpdateData] = useState({
- tag_name: '',
- content: '',
- });
-
- const getOptions = async () => {
- const res = await API.get('/api/option/');
- const { success, message, data } = res.data;
- if (success) {
- let newInputs = {};
- data.forEach((item) => {
- if (item.key in inputs) {
- newInputs[item.key] = item.value;
- }
- });
- setInputs(newInputs);
- } else {
- showError(message);
- }
- };
-
- useEffect(() => {
- getOptions().then();
- }, []);
-
- const updateOption = async (key, value) => {
- setLoading(true);
- const res = await API.put('/api/option', {
- key,
- value,
- });
- const { success, message } = res.data;
- if (success) {
- setInputs((inputs) => ({ ...inputs, [key]: value }));
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- const handleInputChange = async (e, { name, value }) => {
- setInputs((inputs) => ({ ...inputs, [name]: value }));
- };
-
- const submitNotice = async () => {
- await updateOption('Notice', inputs.Notice);
- };
-
- const submitFooter = async () => {
- await updateOption('Footer', inputs.Footer);
- };
-
- const submitSystemName = async () => {
- await updateOption('SystemName', inputs.SystemName);
- };
-
- const submitHomePageLink = async () => {
- await updateOption('HomePageLink', inputs.HomePageLink);
- };
-
- const submitAbout = async () => {
- await updateOption('About', inputs.About);
- };
-
- const openGitHubRelease = () => {
- window.location = 'https://github.com/Rain-kl/ATSFlare/releases/latest';
- };
-
- const checkUpdate = async () => {
- const res = await API.get('/api/update/latest-release');
- const { success, message, data } = res.data;
- if (!success) {
- showError(message);
- return;
- }
- const { tag_name, body } = data;
- if (
- hasBuildVersion() &&
- normalizeVersion(tag_name) === getBuildVersion()
- ) {
- showSuccess(`已是最新版本:${tag_name}`);
- } else {
- setUpdateData({
- tag_name: tag_name,
- content: marked.parse(body),
- });
- setShowUpdateModal(true);
- }
- };
-
- return (
-
-
- 检查更新
-
-
-
- 保存公告
-
-
-
-
-
- 设置系统名称
-
-
-
- 设置首页链接
-
-
-
- 保存关于
-
-
-
- 设置页脚
-
-
- setShowUpdateModal(false)}
- onOpen={() => setShowUpdateModal(true)}
- open={showUpdateModal}
- >
- 新版本:{updateData.tag_name}
-
-
-
-
-
-
-
-
-
-
- );
-};
-
-export default OtherSetting;
diff --git a/atsf_server/web/src/components/PasswordResetConfirm.js b/atsf_server/web/src/components/PasswordResetConfirm.js
deleted file mode 100644
index fe18f7ca..00000000
--- a/atsf_server/web/src/components/PasswordResetConfirm.js
+++ /dev/null
@@ -1,76 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
-import { API, copy, showError, showSuccess } from '../helpers';
-import { useSearchParams } from 'react-router-dom';
-
-const PasswordResetConfirm = () => {
- const [inputs, setInputs] = useState({
- email: '',
- token: '',
- });
- const { email, token } = inputs;
-
- const [loading, setLoading] = useState(false);
-
- const [searchParams, setSearchParams] = useSearchParams();
- useEffect(() => {
- let token = searchParams.get('token');
- let email = searchParams.get('email');
- setInputs({
- token,
- email,
- });
- }, []);
-
- async function handleSubmit(e) {
- if (!email) return;
- setLoading(true);
- const res = await API.post(`/api/user/reset`, {
- email,
- token,
- });
- const { success, message } = res.data;
- if (success) {
- let password = res.data.data;
- await copy(password);
- showSuccess(`密码已重置并已复制到剪贴板:${password}`);
- } else {
- showError(message);
- }
- setLoading(false);
- }
-
- return (
-
-
-
-
-
-
- );
-};
-
-export default PasswordResetConfirm;
diff --git a/atsf_server/web/src/components/PasswordResetForm.js b/atsf_server/web/src/components/PasswordResetForm.js
deleted file mode 100644
index 8de95d6c..00000000
--- a/atsf_server/web/src/components/PasswordResetForm.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react';
-import { API, showError, showInfo, showSuccess } from '../helpers';
-import Turnstile from 'react-turnstile';
-
-const PasswordResetForm = () => {
- const [inputs, setInputs] = useState({
- email: '',
- });
- 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;
- setInputs((inputs) => ({ ...inputs, [name]: value }));
- }
-
- 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}&turnstile=${turnstileToken}`
- );
- const { success, message } = res.data;
- if (success) {
- showSuccess('重置邮件发送成功,请检查邮箱!');
- setInputs({ ...inputs, email: '' });
- } else {
- showError(message);
- }
- setLoading(false);
- }
-
- return (
-
-
-
-
-
-
- );
-};
-
-export default PasswordResetForm;
diff --git a/atsf_server/web/src/components/PersonalSetting.js b/atsf_server/web/src/components/PersonalSetting.js
deleted file mode 100644
index 76c0cdcc..00000000
--- a/atsf_server/web/src/components/PersonalSetting.js
+++ /dev/null
@@ -1,217 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Divider, Form, Header, Image, Modal } from 'semantic-ui-react';
-import { Link } from 'react-router-dom';
-import { API, copy, showError, showInfo, showSuccess } from '../helpers';
-import Turnstile from 'react-turnstile';
-
-const PersonalSetting = () => {
- const [inputs, setInputs] = useState({
- wechat_verification_code: '',
- email_verification_code: '',
- email: '',
- });
- 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);
- }
- }
- }, []);
-
- const handleInputChange = (e, { name, value }) => {
- 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(
- `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
- );
- const { success, message } = res.data;
- if (success) {
- showSuccess('微信账户绑定成功!');
- setShowWeChatBindModal(false);
- } else {
- showError(message);
- }
- };
-
- const openGitHubOAuth = () => {
- window.open(
- `https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
- );
- };
-
- const sendVerificationCode = async () => {
- if (inputs.email === '') return;
- 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);
- };
-
- 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}`
- );
- const { success, message } = res.data;
- if (success) {
- showSuccess('邮箱账户绑定成功!');
- setShowEmailBindModal(false);
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- return (
-
-
-
-
-
-
- {status.wechat_login && (
-
- )}
-
setShowWeChatBindModal(false)}
- onOpen={() => setShowWeChatBindModal(true)}
- open={showWeChatBindModal}
- size={'mini'}
- >
-
-
-
-
-
- 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
-
-
-
-
-
-
-
-
- {status.github_oauth && (
-
- )}
-
-
setShowEmailBindModal(false)}
- onOpen={() => setShowEmailBindModal(true)}
- open={showEmailBindModal}
- size={'tiny'}
- style={{ maxWidth: '450px' }}
- >
- 绑定邮箱地址
-
-
-
- 获取验证码
-
- }
- />
-
- {turnstileEnabled ? (
- {
- setTurnstileToken(token);
- }}
- />
- ) : (
- <>>
- )}
-
-
-
-
-
-
- );
-};
-
-export default PersonalSetting;
diff --git a/atsf_server/web/src/components/PrivateRoute.js b/atsf_server/web/src/components/PrivateRoute.js
deleted file mode 100644
index f7cc7248..00000000
--- a/atsf_server/web/src/components/PrivateRoute.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { Navigate } from 'react-router-dom';
-
-import { history } from '../helpers';
-
-
-function PrivateRoute({ children }) {
- if (!localStorage.getItem('user')) {
- return ;
- }
- return children;
-}
-
-export { PrivateRoute };
\ No newline at end of file
diff --git a/atsf_server/web/src/components/RegisterForm.js b/atsf_server/web/src/components/RegisterForm.js
deleted file mode 100644
index b94ea20b..00000000
--- a/atsf_server/web/src/components/RegisterForm.js
+++ /dev/null
@@ -1,193 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button,
- Form,
- Grid,
- Header,
- Image,
- Message,
- Segment,
-} 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({
- username: '',
- password: '',
- password2: '',
- email: '',
- 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);
- }
- }
- });
-
- let navigate = useNavigate();
-
- function handleChange(e) {
- const { name, value } = e.target;
- console.log(name, value);
- setInputs((inputs) => ({ ...inputs, [name]: value }));
- }
-
- async function handleSubmit(e) {
- if (password.length < 8) {
- showInfo('密码长度不得小于 8 位!');
- return;
- }
- if (password !== password2) {
- showInfo('两次输入的密码不一致');
- return;
- }
- if (username && password) {
- 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');
- showSuccess('注册成功!');
- } else {
- showError(message);
- }
- setLoading(false);
- }
- }
-
- const sendVerificationCode = async () => {
- if (inputs.email === '') return;
- 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 (
-
-
-
-
-
- 已有账户?
-
- 点击登录
-
-
-
-
- );
-};
-
-export default RegisterForm;
diff --git a/atsf_server/web/src/components/SystemSetting.js b/atsf_server/web/src/components/SystemSetting.js
deleted file mode 100644
index 45a98063..00000000
--- a/atsf_server/web/src/components/SystemSetting.js
+++ /dev/null
@@ -1,399 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Divider, Form, Grid, Header, Message } from 'semantic-ui-react';
-import { API, removeTrailingSlash, showError } from '../helpers';
-
-const SystemSetting = () => {
- let [inputs, setInputs] = useState({
- PasswordLoginEnabled: '',
- PasswordRegisterEnabled: '',
- EmailVerificationEnabled: '',
- GitHubOAuthEnabled: '',
- GitHubClientId: '',
- GitHubClientSecret: '',
- Notice: '',
- SMTPServer: '',
- SMTPPort: '',
- SMTPAccount: '',
- SMTPToken: '',
- ServerAddress: '',
- Footer: '',
- WeChatAuthEnabled: '',
- WeChatServerAddress: '',
- WeChatServerToken: '',
- WeChatAccountQRCodeImageURL: '',
- TurnstileCheckEnabled: '',
- TurnstileSiteKey: '',
- TurnstileSecretKey: '',
- RegisterEnabled: '',
- });
- const [originInputs, setOriginInputs] = useState({});
- let [loading, setLoading] = useState(false);
-
- const getOptions = async () => {
- const res = await API.get('/api/option/');
- const { success, message, data } = res.data;
- if (success) {
- let newInputs = {};
- data.forEach((item) => {
- newInputs[item.key] = item.value;
- });
- setInputs(newInputs);
- setOriginInputs(newInputs);
- } else {
- showError(message);
- }
- };
-
- useEffect(() => {
- getOptions().then();
- }, []);
-
- const updateOption = async (key, value) => {
- setLoading(true);
- switch (key) {
- case 'PasswordLoginEnabled':
- case 'PasswordRegisterEnabled':
- case 'EmailVerificationEnabled':
- case 'GitHubOAuthEnabled':
- case 'WeChatAuthEnabled':
- case 'TurnstileCheckEnabled':
- case 'RegisterEnabled':
- value = inputs[key] === 'true' ? 'false' : 'true';
- break;
- default:
- break;
- }
- const res = await API.put('/api/option/', {
- key,
- value,
- });
- const { success, message } = res.data;
- if (success) {
- setInputs((inputs) => ({ ...inputs, [key]: value }));
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- const handleInputChange = async (e, { name, value }) => {
- if (
- name === 'Notice' ||
- name.startsWith('SMTP') ||
- name === 'ServerAddress' ||
- name === 'GitHubClientId' ||
- name === 'GitHubClientSecret' ||
- name === 'WeChatServerAddress' ||
- name === 'WeChatServerToken' ||
- name === 'WeChatAccountQRCodeImageURL' ||
- name === 'TurnstileSiteKey' ||
- name === 'TurnstileSecretKey'
- ) {
- setInputs((inputs) => ({ ...inputs, [name]: value }));
- } else {
- await updateOption(name, value);
- }
- };
-
- const submitServerAddress = async () => {
- let ServerAddress = removeTrailingSlash(inputs.ServerAddress);
- await updateOption('ServerAddress', ServerAddress);
- };
-
- const submitSMTP = async () => {
- if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
- await updateOption('SMTPServer', inputs.SMTPServer);
- }
- if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) {
- await updateOption('SMTPAccount', inputs.SMTPAccount);
- }
- if (
- originInputs['SMTPPort'] !== inputs.SMTPPort &&
- inputs.SMTPPort !== ''
- ) {
- await updateOption('SMTPPort', inputs.SMTPPort);
- }
- if (
- originInputs['SMTPToken'] !== inputs.SMTPToken &&
- inputs.SMTPToken !== ''
- ) {
- await updateOption('SMTPToken', inputs.SMTPToken);
- }
- };
-
- const submitWeChat = async () => {
- if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
- await updateOption(
- 'WeChatServerAddress',
- removeTrailingSlash(inputs.WeChatServerAddress)
- );
- }
- if (
- originInputs['WeChatAccountQRCodeImageURL'] !==
- inputs.WeChatAccountQRCodeImageURL
- ) {
- await updateOption(
- 'WeChatAccountQRCodeImageURL',
- inputs.WeChatAccountQRCodeImageURL
- );
- }
- if (
- originInputs['WeChatServerToken'] !== inputs.WeChatServerToken &&
- inputs.WeChatServerToken !== ''
- ) {
- await updateOption('WeChatServerToken', inputs.WeChatServerToken);
- }
- };
-
- const submitGitHubOAuth = async () => {
- if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) {
- await updateOption('GitHubClientId', inputs.GitHubClientId);
- }
- if (
- originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret &&
- inputs.GitHubClientSecret !== ''
- ) {
- await updateOption('GitHubClientSecret', inputs.GitHubClientSecret);
- }
- };
-
- 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 (
-
-
-
-
-
-
- 更新服务器地址
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 配置 SMTP
- 用以支持系统的邮件发送
-
-
-
-
-
-
-
- 保存 SMTP 设置
-
-
- 配置 GitHub OAuth App
-
- 用以支持通过 GitHub 进行登录注册,
-
- 点击此处
-
- 管理你的 GitHub OAuth App
-
-
-
- Homepage URL 填 {inputs.ServerAddress}
- ,Authorization callback URL 填{' '}
- {`${inputs.ServerAddress}/oauth/github`}
-
-
-
-
-
-
- 保存 GitHub OAuth 设置
-
-
-
- 配置 WeChat Server
-
- 用以支持通过微信进行登录注册,
-
- 点击此处
-
- 了解 WeChat Server
-
-
-
-
-
-
-
-
- 保存 WeChat Server 设置
-
-
-
- 配置 Turnstile
-
- 用以支持用户校验,
-
- 点击此处
-
- 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
-
-
-
-
-
-
-
- 保存 Turnstile 设置
-
-
-
-
- );
-};
-
-export default SystemSetting;
diff --git a/atsf_server/web/src/components/UsersTable.js b/atsf_server/web/src/components/UsersTable.js
deleted file mode 100644
index 67cb787c..00000000
--- a/atsf_server/web/src/components/UsersTable.js
+++ /dev/null
@@ -1,317 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button,
- Form,
- Label,
- Pagination,
- Popup,
- Table,
-} from 'semantic-ui-react';
-import { Link } from 'react-router-dom';
-import { API, showError, showSuccess } from '../helpers';
-
-import { ITEMS_PER_PAGE } from '../constants';
-
-function renderRole(role) {
- switch (role) {
- case 1:
- return ;
- case 10:
- return ;
- case 100:
- return ;
- default:
- return ;
- }
-}
-
-const UsersTable = () => {
- const [users, setUsers] = useState([]);
- const [loading, setLoading] = useState(true);
- const [activePage, setActivePage] = useState(1);
- const [searchKeyword, setSearchKeyword] = useState('');
- const [searching, setSearching] = useState(false);
-
- const loadUsers = async (startIdx) => {
- const res = await API.get(`/api/user/?p=${startIdx}`);
- const { success, message, data } = res.data;
- if (success) {
- if (startIdx === 0) {
- setUsers(data);
- } else {
- let newUsers = users;
- newUsers.push(...data);
- setUsers(newUsers);
- }
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- const onPaginationChange = (e, { 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(0)
- .then()
- .catch((reason) => {
- showError(reason);
- });
- }, []);
-
- const manageUser = (username, action, idx) => {
- (async () => {
- const res = await API.post('/api/user/manage', {
- username,
- action,
- });
- const { success, message } = res.data;
- if (success) {
- showSuccess('操作成功完成!');
- 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) => {
- switch (status) {
- case 1:
- return ;
- case 2:
- return (
-
- );
- default:
- return (
-
- );
- }
- };
-
- 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, { value }) => {
- setSearchKeyword(value.trim());
- };
-
- const sortUser = (key) => {
- if (users.length === 0) return;
- setLoading(true);
- let sortedUsers = [...users];
- sortedUsers.sort((a, b) => {
- return ('' + a[key]).localeCompare(b[key]);
- });
- if (sortedUsers[0].id === users[0].id) {
- sortedUsers.reverse();
- }
- setUsers(sortedUsers);
- setLoading(false);
- };
-
- return (
- <>
-
-
-
-
-
-
- {
- sortUser('username');
- }}
- >
- 用户名
-
- {
- sortUser('display_name');
- }}
- >
- 显示名称
-
- {
- sortUser('email');
- }}
- >
- 邮箱地址
-
- {
- sortUser('role');
- }}
- >
- 用户角色
-
- {
- sortUser('status');
- }}
- >
- 状态
-
- 操作
-
-
-
-
- {users
- .slice(
- (activePage - 1) * ITEMS_PER_PAGE,
- activePage * ITEMS_PER_PAGE
- )
- .map((user, idx) => {
- if (user.deleted) return <>>;
- return (
-
- {user.username}
- {user.display_name}
- {user.email ? user.email : '无'}
- {renderRole(user.role)}
- {renderStatus(user.status)}
-
-
-
-
-
- 删除
-
- }
- on='click'
- flowing
- hoverable
- >
-
-
-
-
-
-
-
- );
- })}
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export default UsersTable;
diff --git a/atsf_server/web/src/constants/common.constant.js b/atsf_server/web/src/constants/common.constant.js
deleted file mode 100644
index 1a37d5f6..00000000
--- a/atsf_server/web/src/constants/common.constant.js
+++ /dev/null
@@ -1 +0,0 @@
-export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!
diff --git a/atsf_server/web/src/constants/index.js b/atsf_server/web/src/constants/index.js
deleted file mode 100644
index 8f9ba808..00000000
--- a/atsf_server/web/src/constants/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export * from './toast.constants';
-export * from './user.constants';
-export * from './common.constant';
\ No newline at end of file
diff --git a/atsf_server/web/src/constants/toast.constants.js b/atsf_server/web/src/constants/toast.constants.js
deleted file mode 100644
index 8b212350..00000000
--- a/atsf_server/web/src/constants/toast.constants.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export const toastConstants = {
- SUCCESS_TIMEOUT: 500,
- INFO_TIMEOUT: 3000,
- ERROR_TIMEOUT: 5000,
- WARNING_TIMEOUT: 10000,
- NOTICE_TIMEOUT: 20000
-};
diff --git a/atsf_server/web/src/constants/user.constants.js b/atsf_server/web/src/constants/user.constants.js
deleted file mode 100644
index 2680d8ef..00000000
--- a/atsf_server/web/src/constants/user.constants.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export const userConstants = {
- REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
- REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
- REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
-
- LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
- LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
- LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
-
- LOGOUT: 'USERS_LOGOUT',
-
- GETALL_REQUEST: 'USERS_GETALL_REQUEST',
- GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
- GETALL_FAILURE: 'USERS_GETALL_FAILURE',
-
- DELETE_REQUEST: 'USERS_DELETE_REQUEST',
- DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
- DELETE_FAILURE: 'USERS_DELETE_FAILURE'
-};
diff --git a/atsf_server/web/src/context/Status/index.js b/atsf_server/web/src/context/Status/index.js
deleted file mode 100644
index 71f0682b..00000000
--- a/atsf_server/web/src/context/Status/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// contexts/User/index.jsx
-
-import React from 'react';
-import { initialState, reducer } from './reducer';
-
-export const StatusContext = React.createContext({
- state: initialState,
- dispatch: () => null,
-});
-
-export const StatusProvider = ({ children }) => {
- const [state, dispatch] = React.useReducer(reducer, initialState);
-
- return (
-
- {children}
-
- );
-};
\ No newline at end of file
diff --git a/atsf_server/web/src/context/Status/reducer.js b/atsf_server/web/src/context/Status/reducer.js
deleted file mode 100644
index ec9ac6ae..00000000
--- a/atsf_server/web/src/context/Status/reducer.js
+++ /dev/null
@@ -1,20 +0,0 @@
-export const reducer = (state, action) => {
- switch (action.type) {
- case 'set':
- return {
- ...state,
- status: action.payload,
- };
- case 'unset':
- return {
- ...state,
- status: undefined,
- };
- default:
- return state;
- }
-};
-
-export const initialState = {
- status: undefined,
-};
diff --git a/atsf_server/web/src/context/User/index.js b/atsf_server/web/src/context/User/index.js
deleted file mode 100644
index c6671591..00000000
--- a/atsf_server/web/src/context/User/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// contexts/User/index.jsx
-
-import React from "react"
-import { reducer, initialState } from "./reducer"
-
-export const UserContext = React.createContext({
- state: initialState,
- dispatch: () => null
-})
-
-export const UserProvider = ({ children }) => {
- const [state, dispatch] = React.useReducer(reducer, initialState)
-
- return (
-
- { children }
-
- )
-}
\ No newline at end of file
diff --git a/atsf_server/web/src/context/User/reducer.js b/atsf_server/web/src/context/User/reducer.js
deleted file mode 100644
index 9ed1d809..00000000
--- a/atsf_server/web/src/context/User/reducer.js
+++ /dev/null
@@ -1,21 +0,0 @@
-export const reducer = (state, action) => {
- switch (action.type) {
- case 'login':
- return {
- ...state,
- user: action.payload
- };
- case 'logout':
- return {
- ...state,
- user: undefined
- };
-
- default:
- return state;
- }
-};
-
-export const initialState = {
- user: undefined
-};
\ No newline at end of file
diff --git a/atsf_server/web/src/helpers/api.js b/atsf_server/web/src/helpers/api.js
deleted file mode 100644
index 35fdb1e9..00000000
--- a/atsf_server/web/src/helpers/api.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { showError } from './utils';
-import axios from 'axios';
-
-export const API = axios.create({
- baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '',
-});
-
-API.interceptors.response.use(
- (response) => response,
- (error) => {
- showError(error);
- }
-);
diff --git a/atsf_server/web/src/helpers/auth-header.js b/atsf_server/web/src/helpers/auth-header.js
deleted file mode 100644
index a8fe5f5a..00000000
--- a/atsf_server/web/src/helpers/auth-header.js
+++ /dev/null
@@ -1,10 +0,0 @@
-export function authHeader() {
- // return authorization header with jwt token
- let user = JSON.parse(localStorage.getItem('user'));
-
- if (user && user.token) {
- return { 'Authorization': 'Bearer ' + user.token };
- } else {
- return {};
- }
-}
\ No newline at end of file
diff --git a/atsf_server/web/src/helpers/history.js b/atsf_server/web/src/helpers/history.js
deleted file mode 100644
index 629039e5..00000000
--- a/atsf_server/web/src/helpers/history.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { createBrowserHistory } from 'history';
-
-export const history = createBrowserHistory();
\ No newline at end of file
diff --git a/atsf_server/web/src/helpers/index.js b/atsf_server/web/src/helpers/index.js
deleted file mode 100644
index 505a8cf9..00000000
--- a/atsf_server/web/src/helpers/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from './history';
-export * from './auth-header';
-export * from './utils';
-export * from './api';
\ No newline at end of file
diff --git a/atsf_server/web/src/helpers/utils.js b/atsf_server/web/src/helpers/utils.js
deleted file mode 100644
index 11ba15a2..00000000
--- a/atsf_server/web/src/helpers/utils.js
+++ /dev/null
@@ -1,181 +0,0 @@
-import { toast } from 'react-toastify';
-import { toastConstants } from '../constants';
-
-export function isAdmin() {
- let user = localStorage.getItem('user');
- if (!user) return false;
- user = JSON.parse(user);
- return user.role >= 10;
-}
-
-export function isRoot() {
- let user = localStorage.getItem('user');
- if (!user) return false;
- user = JSON.parse(user);
- return user.role >= 100;
-}
-
-export function getSystemName() {
- let system_name = localStorage.getItem('system_name');
- if (!system_name) return 'ATSFlare';
- return system_name;
-}
-
-export function getFooterHTML() {
- return localStorage.getItem('footer_html');
-}
-
-export function normalizeVersion(value) {
- if (!value) return '';
- return String(value).trim().replace(/^v/i, '');
-}
-
-export function getBuildVersion() {
- return normalizeVersion(process.env.REACT_APP_VERSION);
-}
-
-export function hasBuildVersion() {
- return getBuildVersion() !== '';
-}
-
-export async function copy(text, label) {
- let okay = true;
- try {
- await navigator.clipboard.writeText(text);
- } catch (e) {
- okay = false;
- console.error(e);
- }
- if (okay && label) {
- showSuccess(`${label} 已复制`);
- }
- return okay;
-}
-
-export function isMobile() {
- return window.innerWidth <= 600;
-}
-
-let showErrorOptions = { autoClose: toastConstants.ERROR_TIMEOUT };
-let showWarningOptions = { autoClose: toastConstants.WARNING_TIMEOUT };
-let showSuccessOptions = { autoClose: toastConstants.SUCCESS_TIMEOUT };
-let showInfoOptions = { autoClose: toastConstants.INFO_TIMEOUT };
-let showNoticeOptions = { autoClose: false };
-
-if (isMobile()) {
- showErrorOptions.position = 'top-center';
- // showErrorOptions.transition = 'flip';
-
- showSuccessOptions.position = 'top-center';
- // showSuccessOptions.transition = 'flip';
-
- showInfoOptions.position = 'top-center';
- // showInfoOptions.transition = 'flip';
-
- showNoticeOptions.position = 'top-center';
- // showNoticeOptions.transition = 'flip';
-}
-
-export function showError(error) {
- console.error(error);
- if (error.message) {
- if (error.name === 'AxiosError') {
- switch (error.response.status) {
- case 401:
- // toast.error('错误:未登录或登录已过期,请重新登录!', showErrorOptions);
- window.location.href = '/login?expired=true';
- break;
- case 429:
- toast.error('错误:请求次数过多,请稍后再试!', showErrorOptions);
- break;
- case 500:
- toast.error('错误:服务器内部错误,请联系管理员!', showErrorOptions);
- break;
- case 405:
- toast.info('本站仅作演示之用,无服务端!');
- break;
- default:
- toast.error('错误:' + error.message, showErrorOptions);
- }
- return;
- }
- toast.error('错误:' + error.message, showErrorOptions);
- } else {
- toast.error('错误:' + error, showErrorOptions);
- }
-}
-
-export function showWarning(message) {
- toast.warn(message, showWarningOptions);
-}
-
-export function showSuccess(message) {
- toast.success(message, showSuccessOptions);
-}
-
-export function showInfo(message) {
- toast.info(message, showInfoOptions);
-}
-
-export function showNotice(message) {
- toast.info(message, showNoticeOptions);
-}
-
-export function openPage(url) {
- window.open(url);
-}
-
-export function removeTrailingSlash(url) {
- if (url.endsWith('/')) {
- return url.slice(0, -1);
- } else {
- return url;
- }
-}
-
-export function timestamp2string(timestamp) {
- let date = new Date(timestamp * 1000);
- let year = date.getFullYear().toString();
- let month = (date.getMonth() + 1).toString();
- let day = date.getDate().toString();
- let hour = date.getHours().toString();
- let minute = date.getMinutes().toString();
- let second = date.getSeconds().toString();
- if (month.length === 1) {
- month = '0' + month;
- }
- if (day.length === 1) {
- day = '0' + day;
- }
- if (hour.length === 1) {
- hour = '0' + hour;
- }
- if (minute.length === 1) {
- minute = '0' + minute;
- }
- if (second.length === 1) {
- second = '0' + second;
- }
- return (
- year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
- );
-}
-
-export function formatDateTime(value) {
- if (!value) return '无';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- return date.toLocaleString('zh-CN', { hour12: false });
-}
-
-export function timeAgo(value) {
- if (!value) return '暂无';
- const date = new Date(value);
- if (Number.isNaN(date.getTime())) return value;
- const diff = Math.floor((Date.now() - date.getTime()) / 1000);
- if (diff < 0) return '刚刚';
- if (diff < 60) return `${diff} 秒前`;
- if (diff < 3600) return `${Math.floor(diff / 60)} 分钟前`;
- if (diff < 86400) return `${Math.floor(diff / 3600)} 小时前`;
- return `${Math.floor(diff / 86400)} 天前`;
-}
diff --git a/atsf_server/web/src/index.css b/atsf_server/web/src/index.css
deleted file mode 100644
index 713b9594..00000000
--- a/atsf_server/web/src/index.css
+++ /dev/null
@@ -1,74 +0,0 @@
-body {
- margin: 0;
- padding-top: 55px;
- overflow-y: scroll;
- font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- scrollbar-width: none;
-}
-
-body::-webkit-scrollbar {
- display: none;
-}
-
-code {
- font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
-}
-
-.main-content {
- padding: 4px;
-}
-
-.small-icon .icon {
- font-size: 1em !important;
-}
-
-.custom-footer {
- font-size: 1.1em;
-}
-
-.page-toolbar {
- display: flex;
- justify-content: space-between;
- align-items: flex-start;
- gap: 16px;
- margin-bottom: 20px;
-}
-
-.page-subtitle {
- margin-top: 6px;
- color: rgba(0, 0, 0, 0.65);
-}
-
-.atsf-table td {
- vertical-align: top !important;
-}
-
-.atsf-pre {
- white-space: pre-wrap;
- word-break: break-word;
- background: #f7f8fa;
- border: 1px solid #e3e6ea;
- border-radius: 8px;
- padding: 12px;
- font-size: 12px;
- line-height: 1.5;
-}
-
-.table-meta {
- margin-top: 6px;
- color: rgba(0, 0, 0, 0.58);
- font-size: 12px;
-}
-
-@media only screen and (max-width: 600px) {
- .hide-on-mobile {
- display: none !important;
- }
-
- .page-toolbar {
- flex-direction: column;
- align-items: stretch;
- }
-}
diff --git a/atsf_server/web/src/index.js b/atsf_server/web/src/index.js
deleted file mode 100644
index eca5c3c0..00000000
--- a/atsf_server/web/src/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom/client';
-import { BrowserRouter } from 'react-router-dom';
-import { Container } from 'semantic-ui-react';
-import App from './App';
-import Header from './components/Header';
-import Footer from './components/Footer';
-import 'semantic-ui-css/semantic.min.css';
-import './index.css';
-import { UserProvider } from './context/User';
-import { ToastContainer } from 'react-toastify';
-import 'react-toastify/dist/ReactToastify.css';
-import { StatusProvider } from './context/Status';
-
-const root = ReactDOM.createRoot(document.getElementById('root'));
-root.render(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-);
diff --git a/atsf_server/web/src/pages/About/index.js b/atsf_server/web/src/pages/About/index.js
deleted file mode 100644
index e216f37a..00000000
--- a/atsf_server/web/src/pages/About/index.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Header, Segment } from 'semantic-ui-react';
-import { API, showError } from '../../helpers';
-import { marked } from 'marked';
-
-const About = () => {
- const [about, setAbout] = useState('');
- const [aboutLoaded, setAboutLoaded] = useState(false);
-
- const displayAbout = async () => {
- setAbout(localStorage.getItem('about') || '');
- const res = await API.get('/api/about');
- const { success, message, data } = res.data;
- if (success) {
- let HTMLAbout = marked.parse(data);
- setAbout(HTMLAbout);
- localStorage.setItem('about', HTMLAbout);
- } else {
- showError(message);
- setAbout('加载关于内容失败...');
- }
- setAboutLoaded(true);
- };
-
- useEffect(() => {
- displayAbout().then();
- }, []);
-
- return (
- <>
-
- {aboutLoaded && about === '' ? (
- <>
-
- 可在设置页面设置关于内容,支持 HTML & Markdown
- 项目仓库地址:
-
- https://github.com/Rain-kl/ATSFlare
-
- >
- ) : (
- <>
-
- >
- )}
-
- >
- );
-};
-
-export default About;
diff --git a/atsf_server/web/src/pages/ApplyLog/index.js b/atsf_server/web/src/pages/ApplyLog/index.js
deleted file mode 100644
index 6d70fd87..00000000
--- a/atsf_server/web/src/pages/ApplyLog/index.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Form, Header, Label, Segment, Table } from 'semantic-ui-react';
-import { API, formatDateTime, showError } from '../../helpers';
-
-const ApplyLog = () => {
- const [logs, setLogs] = useState([]);
- const [keyword, setKeyword] = useState('');
- const [loading, setLoading] = useState(false);
-
- const loadLogs = async (nodeId = '') => {
- setLoading(true);
- const path = nodeId ? `/api/apply-logs/?node_id=${encodeURIComponent(nodeId)}` : '/api/apply-logs/';
- const res = await API.get(path);
- const { success, message, data } = res.data;
- if (success) {
- setLogs(data || []);
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- useEffect(() => {
- loadLogs().then();
- }, []);
-
- const submit = async () => {
- await loadLogs(keyword.trim());
- };
-
- return (
-
-
- 按节点查看配置应用成功或失败记录。
-
- setKeyword(value)}
- />
-
-
-
-
-
- Node ID
- 版本
- 结果
- 信息
- 时间
-
-
-
- {logs.map((log) => (
-
- {log.node_id}
- {log.version}
-
- {log.result === 'success' ? : }
-
- {log.message || '无'}
- {formatDateTime(log.created_at)}
-
- ))}
-
-
-
- );
-};
-
-export default ApplyLog;
diff --git a/atsf_server/web/src/pages/ConfigVersion/index.js b/atsf_server/web/src/pages/ConfigVersion/index.js
deleted file mode 100644
index 36a7a885..00000000
--- a/atsf_server/web/src/pages/ConfigVersion/index.js
+++ /dev/null
@@ -1,213 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button,
- Divider,
- Header,
- Icon,
- Label,
- Modal,
- Segment,
- Table,
-} from 'semantic-ui-react';
-import { API, formatDateTime, showError, showSuccess } from '../../helpers';
-
-const renderDomainList = (items) => {
- if (!items || items.length === 0) {
- return 无
;
- }
- return (
-
- {items.map((item) => (
- - {item}
- ))}
-
- );
-};
-
-const ConfigVersion = () => {
- const [versions, setVersions] = useState([]);
- const [loading, setLoading] = useState(false);
- const [publishing, setPublishing] = useState(false);
- const [preview, setPreview] = useState(null);
- const [publishPreviewOpen, setPublishPreviewOpen] = useState(false);
- const [previewLoading, setPreviewLoading] = useState(false);
- const [pendingPreview, setPendingPreview] = useState(null);
- const [pendingDiff, setPendingDiff] = useState(null);
-
- const loadVersions = async () => {
- setLoading(true);
- const res = await API.get('/api/config-versions/');
- const { success, message, data } = res.data;
- if (success) {
- setVersions(data || []);
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- useEffect(() => {
- loadVersions().then();
- }, []);
-
- const publishConfig = async () => {
- setPublishing(true);
- const res = await API.post('/api/config-versions/publish');
- const { success, message, data } = res.data;
- if (success) {
- showSuccess(`发布成功,版本 ${data.version}`);
- await loadVersions();
- } else {
- showError(message);
- }
- setPublishing(false);
- return success;
- };
-
- const openPublishPreview = async () => {
- setPreviewLoading(true);
- const [previewRes, diffRes] = await Promise.all([
- API.get('/api/config-versions/preview'),
- API.get('/api/config-versions/diff'),
- ]);
- const previewPayload = previewRes.data;
- const diffPayload = diffRes.data;
- if (!previewPayload.success) {
- showError(previewPayload.message);
- setPreviewLoading(false);
- return;
- }
- if (!diffPayload.success) {
- showError(diffPayload.message);
- setPreviewLoading(false);
- return;
- }
- setPendingPreview(previewPayload.data || null);
- setPendingDiff(diffPayload.data || null);
- setPublishPreviewOpen(true);
- setPreviewLoading(false);
- };
-
- const confirmPublish = async () => {
- const success = await publishConfig();
- if (success) {
- setPublishPreviewOpen(false);
- setPendingPreview(null);
- setPendingDiff(null);
- }
- };
-
- const activateVersion = async (id) => {
- const res = await API.put(`/api/config-versions/${id}/activate`);
- const { success, message, data } = res.data;
- if (success) {
- showSuccess(`已激活版本 ${data.version}`);
- await loadVersions();
- } else {
- showError(message);
- }
- };
-
- return (
-
-
-
-
-
查看历史快照,预览即将发布的配置与变更摘要,或重新激活旧版本。
-
-
-
-
-
-
-
- 版本号
- 状态
- 创建人
- Checksum
- 创建时间
- 操作
-
-
-
- {versions.map((version) => (
-
- {version.version}
-
- {version.is_active ? : }
-
- {version.created_by}
- {(version.checksum || '').slice(0, 16)}...
- {formatDateTime(version.created_at)}
-
-
- {!version.is_active ? (
-
- ) : null}
-
-
- ))}
-
-
-
- setPreview(null)} closeIcon>
- 版本预览
-
- {preview ? (
- <>
-
- {preview.snapshot_json}
-
- {preview.rendered_config}
- >
- ) : null}
-
-
-
- setPublishPreviewOpen(false)} closeIcon>
- 发布前预览
-
- {pendingDiff ? (
- <>
-
- 当前激活版本:{pendingDiff.active_version || '无'}
-
-
-
-
-
- {renderDomainList(pendingDiff.added_domains)}
-
- {renderDomainList(pendingDiff.removed_domains)}
-
- {renderDomainList(pendingDiff.modified_domains)}
- >
- ) : null}
- {pendingPreview ? (
- <>
-
-
- 启用规则数:{pendingPreview.route_count},Checksum:{pendingPreview.checksum}
- {pendingPreview.rendered_config}
- >
- ) : null}
-
-
-
-
-
-
-
- );
-};
-
-export default ConfigVersion;
diff --git a/atsf_server/web/src/pages/File/index.js b/atsf_server/web/src/pages/File/index.js
deleted file mode 100644
index d61f6bab..00000000
--- a/atsf_server/web/src/pages/File/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-import { Header, Segment } from 'semantic-ui-react';
-import FilesTable from '../../components/FilesTable';
-
-const File = () => (
- <>
-
-
-
-
- >
-);
-
-export default File;
diff --git a/atsf_server/web/src/pages/Home/index.js b/atsf_server/web/src/pages/Home/index.js
deleted file mode 100644
index 01d8eeb5..00000000
--- a/atsf_server/web/src/pages/Home/index.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import React, { useContext, useEffect } from 'react';
-import { Card, Grid, Header, Segment } from 'semantic-ui-react';
-import { API, showError, showNotice, timestamp2string } from '../../helpers';
-import { StatusContext } from '../../context/Status';
-
-const Home = () => {
- const [statusState, statusDispatch] = useContext(StatusContext);
- const homePageLink = localStorage.getItem('home_page_link') || '';
-
- const displayNotice = async () => {
- const res = await API.get('/api/notice');
- const { success, message, data } = res.data;
- if (success) {
- let oldNotice = localStorage.getItem('notice');
- if (data !== oldNotice && data !== '') {
- showNotice(data);
- localStorage.setItem('notice', data);
- }
- } else {
- showError(message);
- }
- };
-
- const getStartTimeString = () => {
- const timestamp = statusState?.status?.start_time;
- return timestamp2string(timestamp);
- };
-
- useEffect(() => {
- displayNotice().then();
- }, []);
- return (
- <>
- {homePageLink !== '' ? (
- <>
-
- >
- ) : (
- <>
-
-
-
-
-
-
- 系统信息
- 系统信息总览
-
- 名称:{statusState?.status?.system_name}
- 版本:{statusState?.status?.version}
-
- 源码:
-
- https://github.com/Rain-kl/ATSFlare
-
-
- 启动时间:{getStartTimeString()}
-
-
-
-
-
-
-
- 系统配置
- 系统配置总览
-
-
- 邮箱验证:
- {statusState?.status?.email_verification === true
- ? '已启用'
- : '未启用'}
-
-
- GitHub 身份验证:
- {statusState?.status?.github_oauth === true
- ? '已启用'
- : '未启用'}
-
-
- 微信身份验证:
- {statusState?.status?.wechat_login === true
- ? '已启用'
- : '未启用'}
-
-
- Turnstile 用户校验:
- {statusState?.status?.turnstile_check === true
- ? '已启用'
- : '未启用'}
-
-
-
-
-
-
-
- >
- )}
- >
- );
-};
-
-export default Home;
diff --git a/atsf_server/web/src/pages/ManagedDomain/index.js b/atsf_server/web/src/pages/ManagedDomain/index.js
deleted file mode 100644
index e02f129c..00000000
--- a/atsf_server/web/src/pages/ManagedDomain/index.js
+++ /dev/null
@@ -1,198 +0,0 @@
-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 (
-
-
- 维护精确域名与通配符域名,并为其绑定默认 TLS 证书。
-
-
- setForm({ ...form, domain: value })}
- />
- setForm({ ...form, cert_id: value || '' })}
- />
-
-
- setForm({ ...form, remark: value })}
- />
- setForm({ ...form, enabled: checked })}
- style={{ alignSelf: 'flex-end', marginBottom: '1rem' }}
- />
-
-
- {editingId ? (
-
- ) : null}
-
-
-
-
-
- 域名
- 绑定证书
- 状态
- 备注
- 更新时间
- 操作
-
-
-
- {domains.map((domain) => {
- const certificate = domain.cert_id ? certificateMap.get(domain.cert_id) : null;
- return (
-
- {domain.domain}
- {certificate ? certificate.name : '未绑定'}
-
- {domain.enabled ? : }
-
- {domain.remark || '无'}
- {formatDateTime(domain.updated_at)}
-
-
-
-
-
- );
- })}
-
-
-
- );
-};
-
-export default ManagedDomain;
diff --git a/atsf_server/web/src/pages/Node/index.js b/atsf_server/web/src/pages/Node/index.js
deleted file mode 100644
index c2b3be2d..00000000
--- a/atsf_server/web/src/pages/Node/index.js
+++ /dev/null
@@ -1,291 +0,0 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { Button, Form, Header, Label, Segment, Table } from 'semantic-ui-react';
-import { API, showError, showSuccess, timeAgo } from '../../helpers';
-
-const initialForm = {
- name: '',
- auto_update_enabled: false,
-};
-
-const renderStatus = (status) => {
- if (status === 'online') {
- return ;
- }
- if (status === 'pending') {
- return ;
- }
- return ;
-};
-
-const renderApply = (result) => {
- if (result === 'success') {
- return ;
- }
- if (result === 'failed') {
- return ;
- }
- return ;
-};
-
-const renderUpdateMode = (node) => {
- if (node.update_requested) {
- return ;
- }
- if (node.auto_update_enabled) {
- return ;
- }
- return ;
-};
-
-const Node = () => {
- const [nodes, setNodes] = useState([]);
- const [bootstrap, setBootstrap] = useState({ discovery_token: '' });
- const [loading, setLoading] = useState(false);
- const [submitting, setSubmitting] = useState(false);
- const [form, setForm] = useState(initialForm);
- const [editingId, setEditingId] = useState(null);
- const [, setTick] = useState(0);
- const refreshTimer = useRef(null);
-
- const loadNodes = useCallback(async (silent) => {
- if (!silent) setLoading(true);
- const res = await API.get('/api/nodes/');
- const { success, message, data } = res.data;
- if (success) {
- setNodes(data || []);
- } else {
- showError(message);
- }
- if (!silent) setLoading(false);
- }, []);
-
- const loadBootstrapToken = async () => {
- const res = await API.get('/api/nodes/bootstrap-token');
- const { success, message, data } = res.data;
- if (success) {
- setBootstrap(data || { discovery_token: '' });
- } else {
- showError(message);
- }
- };
-
- const rotateBootstrapToken = async () => {
- setLoading(true);
- const res = await API.post('/api/nodes/bootstrap-token/rotate');
- const { success, message, data } = res.data;
- if (success) {
- setBootstrap(data || { discovery_token: '' });
- showSuccess('全局 discovery token 已重新生成');
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- useEffect(() => {
- loadBootstrapToken().then();
- loadNodes(false).then();
- // Refresh node list and relative time every 30s
- refreshTimer.current = setInterval(() => {
- loadNodes(true);
- setTick((t) => t + 1);
- }, 30000);
- return () => clearInterval(refreshTimer.current);
- }, [loadNodes]);
-
- const resetForm = () => {
- setForm(initialForm);
- setEditingId(null);
- };
-
- const submitNode = async () => {
- setSubmitting(true);
- const payload = {
- name: form.name.trim(),
- auto_update_enabled: form.auto_update_enabled,
- };
- const res = editingId
- ? await API.put(`/api/nodes/${editingId}`, payload)
- : await API.post('/api/nodes/', payload);
- const { success, message } = res.data;
- if (success) {
- showSuccess(editingId ? '节点已更新' : '节点已创建');
- resetForm();
- await loadNodes(false);
- } else {
- showError(message);
- }
- setSubmitting(false);
- };
-
- const beginEdit = (node) => {
- setEditingId(node.id);
- setForm({
- name: node.name || '',
- auto_update_enabled: !!node.auto_update_enabled,
- });
- };
-
- const requestAgentUpdate = async (node) => {
- const res = await API.post(`/api/nodes/${node.id}/agent-update`);
- const { success, message } = res.data;
- if (success) {
- showSuccess(`已向节点 ${node.name} 下发更新指令`);
- await loadNodes(false);
- } else {
- showError(message);
- }
- };
-
- const deleteNode = async (node) => {
- if (
- !window.confirm(
- `确认删除节点“${node.name}”吗?删除后该节点需要重新创建并重新接入。`
- )
- ) {
- return;
- }
- const res = await API.delete(`/api/nodes/${node.id}`);
- const { success, message } = res.data;
- if (success) {
- showSuccess('节点已删除');
- if (editingId === node.id) {
- resetForm();
- }
- await loadNodes(false);
- } else {
- showError(message);
- }
- };
-
- return (
-
-
-
- 创建节点后会直接生成节点专属 auth token;批量部署时可复用全局 discovery
- token 自动注册。
-
-
-
-
-
-
-
-
-
- setForm({ ...form, name: value })}
- />
-
- setForm({ ...form, auto_update_enabled: !!checked })
- }
- style={{ alignSelf: 'flex-end', marginBottom: '0.5rem' }}
- />
-
-
- {editingId ? (
-
- ) : null}
-
-
-
-
-
- 节点名
- Node ID
- Auth Token
- IP
- 状态
- Agent 更新
- Agent / Nginx
- 当前版本
- 最近应用
- 最近心跳
- 错误
- 操作
-
-
-
- {nodes.map((node) => (
-
- {node.name}
- {node.node_id}
-
- {node.agent_token ? (
- <>
- {node.pending ? (
-
- ) : (
-
- )}
-
- {node.agent_token}
-
- >
- ) : (
- '暂无'
- )}
-
- {node.ip}
- {renderStatus(node.status)}
- {renderUpdateMode(node)}
-
- {node.agent_version} / {node.nginx_version || 'unknown'}
-
- {node.current_version || '未应用'}
-
- {renderApply(node.latest_apply_result)}
-
- {node.latest_apply_message || '暂无记录'}
-
-
-
- {node.last_seen_at ? timeAgo(node.last_seen_at) : '暂无'}
-
- {node.last_error || '无'}
-
-
-
-
-
-
- ))}
-
-
-
- );
-};
-
-export default Node;
diff --git a/atsf_server/web/src/pages/NotFound/index.js b/atsf_server/web/src/pages/NotFound/index.js
deleted file mode 100644
index 08a95f9d..00000000
--- a/atsf_server/web/src/pages/NotFound/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react';
-import { Segment, Header } from 'semantic-ui-react';
-
-const NotFound = () => (
- <>
-
-
- 未找到所请求的页面
-
- >
-);
-
-export default NotFound;
diff --git a/atsf_server/web/src/pages/ProxyRoute/index.js b/atsf_server/web/src/pages/ProxyRoute/index.js
deleted file mode 100644
index 315eae46..00000000
--- a/atsf_server/web/src/pages/ProxyRoute/index.js
+++ /dev/null
@@ -1,398 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button,
- Dropdown,
- Form,
- Header,
- Icon,
- Input,
- Label,
- Segment,
- Table,
- TextArea,
-} from 'semantic-ui-react';
-import { API, showError, showSuccess, formatDateTime } from '../../helpers';
-
-const emptyHeader = () => ({ key: '', value: '' });
-
-const parseCustomHeaders = (rawValue) => {
- if (Array.isArray(rawValue)) {
- return rawValue;
- }
- if (!rawValue) {
- return [];
- }
- try {
- const parsed = JSON.parse(rawValue);
- return Array.isArray(parsed) ? parsed : [];
- } catch (error) {
- return [];
- }
-};
-
-const initialForm = {
- domain: '',
- origin_url: '',
- enabled: true,
- enable_https: false,
- cert_id: '',
- redirect_http: false,
- custom_headers: [emptyHeader()],
- remark: '',
-};
-
-const ProxyRoute = () => {
- const [routes, setRoutes] = useState([]);
- 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);
-
- const loadRoutes = async () => {
- setLoading(true);
- const res = await API.get('/api/proxy-routes/');
- const { success, message, data } = res.data;
- if (success) {
- setRoutes(data || []);
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- const loadCertificates = async () => {
- const res = await API.get('/api/tls-certificates/');
- const { success, message, data } = res.data;
- if (success) {
- setCertificates(data || []);
- } else {
- showError(message);
- }
- };
-
- useEffect(() => {
- loadRoutes().then();
- 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 (event) => {
- event?.preventDefault?.();
-
- const payload = {
- ...form,
- domain: form.domain.trim(),
- origin_url: form.origin_url.trim(),
- cert_id: form.enable_https && form.cert_id ? Number(form.cert_id) : null,
- custom_headers: (form.custom_headers || []).filter((item) => item.key.trim() || item.value.trim()),
- remark: form.remark.trim(),
- };
- const res = editingId
- ? await API.put(`/api/proxy-routes/${editingId}`, payload)
- : await API.post('/api/proxy-routes/', payload);
- const { success, message } = res.data;
- if (success) {
- showSuccess(editingId ? '规则已更新' : '规则已创建');
- resetForm();
- await loadRoutes();
- } else {
- showError(message);
- }
- };
-
- const deleteRoute = async (id) => {
- const res = await API.delete(`/api/proxy-routes/${id}`);
- const { success, message } = res.data;
- if (success) {
- showSuccess('规则已删除');
- await loadRoutes();
- } else {
- showError(message);
- }
- };
-
- const publishConfig = async () => {
- setPublishing(true);
- const res = await API.post('/api/config-versions/publish');
- const { success, message, data } = res.data;
- if (success) {
- showSuccess(`发布成功,版本 ${data.version}`);
- } else {
- showError(message);
- }
- setPublishing(false);
- };
-
- const beginEdit = (route) => {
- const customHeaders = parseCustomHeaders(route.custom_headers);
- setEditingId(route.id);
- setForm({
- domain: route.domain,
- origin_url: route.origin_url,
- enabled: route.enabled,
- enable_https: route.enable_https || false,
- cert_id: route.cert_id || '',
- redirect_http: route.redirect_http || false,
- custom_headers: customHeaders.length > 0 ? customHeaders : [emptyHeader()],
- remark: route.remark || '',
- });
- setMatchResult(null);
- };
-
- const updateCustomHeader = (index, field, value) => {
- setForm((current) => ({
- ...current,
- custom_headers: current.custom_headers.map((item, itemIndex) => (
- itemIndex === index ? { ...item, [field]: value } : item
- )),
- }));
- };
-
- const addCustomHeader = () => {
- setForm((current) => ({
- ...current,
- custom_headers: [...(current.custom_headers || []), emptyHeader()],
- }));
- };
-
- const removeCustomHeader = (index) => {
- setForm((current) => {
- const nextHeaders = (current.custom_headers || []).filter((item, itemIndex) => itemIndex !== index);
- return {
- ...current,
- custom_headers: nextHeaders.length > 0 ? nextHeaders : [emptyHeader()],
- };
- });
- };
-
- const certificateOptions = certificates.map((certificate) => ({
- key: certificate.id,
- text: `${certificate.name} (${certificate.not_after ? formatDateTime(certificate.not_after) : 'unknown'})`,
- value: certificate.id,
- }));
-
- return (
-
-
-
-
-
维护 Host 到 Origin 的映射,并可直接触发发布。
-
-
-
-
-
- setForm({ ...form, domain: value })}
- />
- setForm({ ...form, origin_url: value })}
- />
-
-
-
- setForm({
- ...form,
- enable_https: checked,
- cert_id: checked ? form.cert_id : '',
- redirect_http: checked ? form.redirect_http : false,
- })
- }
- style={{ alignSelf: 'flex-end', marginBottom: '1rem' }}
- />
- setForm({ ...form, cert_id: value || '' })}
- />
-
- {form.enable_https ? (
-
- {matching
- ? '正在按域名自动匹配证书...'
- : matchResult?.matched
- ? `已匹配${matchResult.candidate?.match_type === 'exact' ? '精确' : '通配符'}规则 ${matchResult.candidate?.domain},推荐证书:${matchResult.candidate?.certificate_name}`
- : form.domain.trim()
- ? '未找到匹配证书,可继续手动选择。'
- : '输入域名后会自动匹配托管证书。'}
-
- ) : null}
-
- setForm({ ...form, remark: value })}
- />
- setForm({ ...form, enabled: checked })}
- style={{ alignSelf: 'flex-end', marginBottom: '1rem' }}
- />
- setForm({ ...form, redirect_http: checked })}
- style={{ alignSelf: 'flex-end', marginBottom: '1rem' }}
- />
-
-
-
- {(form.custom_headers || []).map((header, index) => (
-
- updateCustomHeader(index, 'key', value)}
- />
- updateCustomHeader(index, 'value', value)}
- />
-
-
-
- ))}
-
-
-
- {editingId ? (
-
- ) : null}
-
-
-
-
-
- 域名
- 源站地址
- HTTPS
- 自定义头
- 状态
- 备注
- 更新时间
- 操作
-
-
-
- {routes.map((route) => (
-
- {route.domain}
- {route.origin_url}
-
- {route.enable_https ? (
-
- ) : (
-
- )}
-
-
- {parseCustomHeaders(route.custom_headers).length > 0 ? (
-
- ) : (
-
- )}
-
-
- {route.enabled ? : }
-
- {route.remark || '无'}
- {formatDateTime(route.updated_at)}
-
-
-
-
-
- ))}
-
-
-
- );
-};
-
-export default ProxyRoute;
diff --git a/atsf_server/web/src/pages/Setting/index.js b/atsf_server/web/src/pages/Setting/index.js
deleted file mode 100644
index e8aae717..00000000
--- a/atsf_server/web/src/pages/Setting/index.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import React from 'react';
-import { Segment, Tab } from 'semantic-ui-react';
-import SystemSetting from '../../components/SystemSetting';
-import { isRoot } from '../../helpers';
-import OtherSetting from '../../components/OtherSetting';
-import PersonalSetting from '../../components/PersonalSetting';
-import OperationSetting from '../../components/OperationSetting';
-
-const Setting = () => {
- let panes = [
- {
- menuItem: '个人设置',
- render: () => (
-
-
-
- ),
- },
- ];
-
- if (isRoot()) {
- panes.push({
- menuItem: '运维设置',
- render: () => (
-
-
-
- ),
- });
- panes.push({
- menuItem: '系统设置',
- render: () => (
-
-
-
- ),
- });
- panes.push({
- menuItem: '其他设置',
- render: () => (
-
-
-
- ),
- });
- }
-
- return (
-
-
-
- );
-};
-
-export default Setting;
diff --git a/atsf_server/web/src/pages/TLSCertificate/index.js b/atsf_server/web/src/pages/TLSCertificate/index.js
deleted file mode 100644
index 316e21df..00000000
--- a/atsf_server/web/src/pages/TLSCertificate/index.js
+++ /dev/null
@@ -1,221 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import {
- Button,
- Form,
- Header,
- Icon,
- Segment,
- Table,
- TextArea,
-} from 'semantic-ui-react';
-import { API, formatDateTime, showError, showSuccess } from '../../helpers';
-
-const initialManualForm = {
- name: '',
- cert_pem: '',
- key_pem: '',
- remark: '',
-};
-
-const initialFileForm = {
- name: '',
- remark: '',
-};
-
-const TLSCertificate = () => {
- const [certificates, setCertificates] = useState([]);
- const [loading, setLoading] = useState(false);
- const [manualForm, setManualForm] = useState(initialManualForm);
- const [fileForm, setFileForm] = useState(initialFileForm);
- const [certFile, setCertFile] = useState(null);
- const [keyFile, setKeyFile] = useState(null);
- const [submittingManual, setSubmittingManual] = useState(false);
- const [submittingFiles, setSubmittingFiles] = useState(false);
-
- const loadCertificates = async () => {
- setLoading(true);
- const res = await API.get('/api/tls-certificates/');
- const { success, message, data } = res.data;
- if (success) {
- setCertificates(data || []);
- } else {
- showError(message);
- }
- setLoading(false);
- };
-
- useEffect(() => {
- loadCertificates().then();
- }, []);
-
- const submitManual = async () => {
- setSubmittingManual(true);
- const payload = {
- name: manualForm.name.trim(),
- cert_pem: manualForm.cert_pem.trim(),
- key_pem: manualForm.key_pem.trim(),
- remark: manualForm.remark.trim(),
- };
- const res = await API.post('/api/tls-certificates/', payload);
- const { success, message } = res.data;
- if (success) {
- showSuccess('证书已导入');
- setManualForm(initialManualForm);
- await loadCertificates();
- } else {
- showError(message);
- }
- setSubmittingManual(false);
- };
-
- const submitFiles = async () => {
- if (!certFile || !keyFile) {
- showError('请选择证书文件和私钥文件');
- return;
- }
- setSubmittingFiles(true);
- const formData = new FormData();
- formData.append('name', fileForm.name.trim());
- formData.append('remark', fileForm.remark.trim());
- formData.append('cert_file', certFile);
- formData.append('key_file', keyFile);
- const res = await API.post('/api/tls-certificates/import-file', formData, {
- headers: { 'Content-Type': 'multipart/form-data' },
- });
- const { success, message } = res.data;
- if (success) {
- showSuccess('证书文件已导入');
- setFileForm(initialFileForm);
- setCertFile(null);
- setKeyFile(null);
- await loadCertificates();
- } else {
- showError(message);
- }
- setSubmittingFiles(false);
- };
-
- const deleteCertificate = async (id) => {
- const res = await API.delete(`/api/tls-certificates/${id}`);
- const { success, message } = res.data;
- if (success) {
- showSuccess('证书已删除');
- await loadCertificates();
- } else {
- showError(message);
- }
- };
-
- return (
-
-
- 支持手动粘贴 PEM 导入或直接上传证书文件。
-
-
- setManualForm({ ...manualForm, name: value })}
- />
- setManualForm({ ...manualForm, remark: value })}
- />
-
- setManualForm({ ...manualForm, cert_pem: value })}
- style={{ minHeight: 140 }}
- />
- setManualForm({ ...manualForm, key_pem: value })}
- style={{ minHeight: 140 }}
- />
-
-
-
-
-
- setFileForm({ ...fileForm, name: value })}
- />
- setFileForm({ ...fileForm, remark: value })}
- />
-
-
- setCertFile(e.target.files?.[0] || null)}
- />
- setKeyFile(e.target.files?.[0] || null)}
- />
-
-
-
-
-
-
-
-
- 名称
- 有效期
- 备注
- 更新时间
- 操作
-
-
-
- {certificates.map((certificate) => (
-
-
-
- {certificate.name}
-
-
- {formatDateTime(certificate.not_before)} ~ {formatDateTime(certificate.not_after)}
-
- {certificate.remark || '无'}
- {formatDateTime(certificate.updated_at)}
-
-
-
-
- ))}
-
-
-
- );
-};
-
-export default TLSCertificate;
diff --git a/atsf_server/web/src/pages/User/AddUser.js b/atsf_server/web/src/pages/User/AddUser.js
deleted file mode 100644
index 73036ada..00000000
--- a/atsf_server/web/src/pages/User/AddUser.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import React, { useState } from 'react';
-import { Button, Form, Header, Segment } from 'semantic-ui-react';
-import { API, showError, showSuccess } from '../../helpers';
-
-const AddUser = () => {
- const originInputs = {
- username: '',
- display_name: '',
- password: '',
- };
- const [inputs, setInputs] = useState(originInputs);
- const { username, display_name, password } = inputs;
-
- const handleInputChange = (e, { name, value }) => {
- setInputs((inputs) => ({ ...inputs, [name]: value }));
- };
-
- const submit = async () => {
- if (inputs.username === '' || inputs.password === '') return;
- const res = await API.post(`/api/user/`, inputs);
- const { success, message } = res.data;
- if (success) {
- showSuccess('用户账户创建成功!');
- setInputs(originInputs);
- } else {
- showError(message);
- }
- };
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export default AddUser;
diff --git a/atsf_server/web/src/pages/User/EditUser.js b/atsf_server/web/src/pages/User/EditUser.js
deleted file mode 100644
index a8d4e7cf..00000000
--- a/atsf_server/web/src/pages/User/EditUser.js
+++ /dev/null
@@ -1,132 +0,0 @@
-import React, { useEffect, useState } from 'react';
-import { Button, Form, Header, Segment } from 'semantic-ui-react';
-import { useParams } from 'react-router-dom';
-import { API, showError, showSuccess } from '../../helpers';
-
-const EditUser = () => {
- const params = useParams();
- const userId = params.id;
- const [loading, setLoading] = useState(true);
- const [inputs, setInputs] = useState({
- username: '',
- display_name: '',
- password: '',
- github_id: '',
- wechat_id: '',
- email: '',
- });
- const { username, display_name, password, github_id, wechat_id, email } =
- inputs;
- const handleInputChange = (e, { name, value }) => {
- setInputs((inputs) => ({ ...inputs, [name]: value }));
- };
-
- const loadUser = async () => {
- let res = undefined;
- if (userId) {
- res = await API.get(`/api/user/${userId}`);
- } else {
- res = await API.get(`/api/user/self`);
- }
- const { success, message, data } = res.data;
- if (success) {
- data.password = '';
- setInputs(data);
- } else {
- showError(message);
- }
- setLoading(false);
- };
- useEffect(() => {
- loadUser().then();
- }, []);
-
- const submit = async () => {
- let res = undefined;
- if (userId) {
- res = await API.put(`/api/user/`, { ...inputs, id: parseInt(userId) });
- } else {
- res = await API.put(`/api/user/self`, inputs);
- }
- const { success, message } = res.data;
- if (success) {
- showSuccess('用户信息更新成功!');
- } else {
- showError(message);
- }
- };
-
- return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-export default EditUser;
diff --git a/atsf_server/web/src/pages/User/index.js b/atsf_server/web/src/pages/User/index.js
deleted file mode 100644
index 29f7437a..00000000
--- a/atsf_server/web/src/pages/User/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-import { Segment, Header } from 'semantic-ui-react';
-import UsersTable from '../../components/UsersTable';
-
-const User = () => (
- <>
-
-
-
-
- >
-);
-
-export default User;
diff --git a/atsf_server/web/tsconfig.json b/atsf_server/web/tsconfig.json
index b7214578..e3f80f53 100644
--- a/atsf_server/web/tsconfig.json
+++ b/atsf_server/web/tsconfig.json
@@ -25,5 +25,5 @@
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
- "exclude": ["node_modules", "build", "src"]
-}
+ "exclude": ["node_modules", "build"]
+}
diff --git a/docs/frontend-revamp-plan.md b/docs/frontend-revamp-plan.md
index 2ba52f43..b10be541 100644
--- a/docs/frontend-revamp-plan.md
+++ b/docs/frontend-revamp-plan.md
@@ -329,23 +329,6 @@ atsf_server/web/
* 日常管理操作均可在新前端完成
* 旧前端仅剩兼容兜底价值
-### 阶段 5:联调、回归与切换
-
-目标:完成替换上线准备。
-
-任务:
-
-1. 对照现有页面与接口完成功能回归
-2. 补齐前端测试与关键页面 E2E
-3. 验证静态资源构建、嵌入、发布流程
-4. 切换默认前端入口并保留回滚预案
-
-验收:
-
-* 所有核心页面通过冒烟测试
-* 构建与部署文档可复现
-* 可在必要时快速回退到旧前端版本
-
---
## 9. 页面与交互设计原则