feat: 完善日志审计功能

- 实现文件系统日志(FilesystemLog)记录文件管理器操作
- 实现操作日志(OperationLog)记录用户操作行为
- 实现数据库SQL日志(DatabaseSQLLog)模型和API
- 实现SSH会话命令记录(SessionCommand)含命令输出和风险等级
- 添加IP提取服务支持X-Real-IP和X-Forwarded-For
- 添加日志自动清理功能
- 修复ProFormSwitch required验证问题
- 修复设置页面默认值问题
- 修复文件上传错误检测逻辑
- 修复资产树key前缀问题
- 添加VNC/RDP设置默认值
- 修复文件管理标题翻译
This commit is contained in:
2026-04-19 06:57:42 +08:00
parent a2a1613384
commit 1f7c491048
42 changed files with 1214 additions and 130 deletions
+7 -2
View File
@@ -160,10 +160,15 @@ const AccessPage = () => {
// 处理树节点双击
const handleNodeDoubleClick = useCallback((node: any) => {
let assetId = node.key as string;
if (typeof assetId === 'string' && assetId.startsWith('asset_')) {
assetId = assetId.substring(6);
}
// 检查是否需要 WOL 唤醒
if (node.extra?.status === 'inactive' && node.extra?.wolEnabled) {
setWolAssetInfo({
id: node.key as string,
id: assetId,
name: node.title as string,
protocol: node.extra?.protocol,
});
@@ -173,7 +178,7 @@ const AccessPage = () => {
// 直接打开连接
openAssetTab({
id: node.key,
id: assetId,
name: node.title,
protocol: node.extra?.protocol,
});
+7 -1
View File
@@ -61,7 +61,13 @@ const AccessSshChooser = ({handleOk, handleCancel, open}: Props) => {
const onCheck: TreeProps['onCheck'] = (checkedKeysValue, {checkedNodes}) => {
// console.log('onCheck', checkedKeysValue, checkedNodes);
let keys = checkedNodes.filter(item => item.isLeaf).map((item) => item.key);
let keys = checkedNodes.filter(item => item.isLeaf).map((item) => {
let key = item.key as string;
if (key.startsWith('asset_')) {
return key.substring(6);
}
return key;
});
setSshAssetKeys(keys as string[]);
};
+3 -6
View File
@@ -762,7 +762,6 @@ const FileSystemPage = forwardRef<FileSystem, Props>(({
console.log(`Upload response - Status: ${xhr.status}, Response: ${xhr.responseText}`);
if (xhr.status >= 200 && xhr.status < 300) {
// 检查响应体是否包含错误信息
let hasError = false;
let errorMessage = '';
@@ -772,15 +771,13 @@ const FileSystemPage = forwardRef<FileSystem, Props>(({
const result = JSON.parse(responseText);
console.log('Upload response parsed:', result);
// 检查是否是标准错误响应格式
if (result.error === true || (result.message && result.code)) {
if (result.error === true || (result.code && result.code !== 1 && result.code !== 0)) {
hasError = true;
errorMessage = result.message || 'Upload failed';
console.error('Upload failed with parsed error:', errorMessage);
}
}
} catch (e) {
// JSON解析失败,可能是成功的空响应,继续处理为成功
console.log('Upload response parse failed, treating as success:', e);
}
@@ -1126,7 +1123,7 @@ const FileSystemPage = forwardRef<FileSystem, Props>(({
return (
<div>
<Drawer title="FileSystem"
<Drawer title={t('fs.title')}
placement="right"
onClose={onClose}
open={open}
@@ -1314,7 +1311,7 @@ const FileSystemPage = forwardRef<FileSystem, Props>(({
)}
<Table
virtual
// scroll={{y: window.innerHeight - 240}}
scroll={{y: 400}}
rowKey={'path'}
columns={fileColumns}
rowSelection={rowSelection}
+12 -2
View File
@@ -69,6 +69,16 @@ const AssetPage = () => {
let [groupId, setGroupId] = useState(searchParams.get('groupId') || '');
let [dataSource, setDataSource] = useState<Asset[]>([]);
const handleTreeSelect = (key: string) => {
if (key.startsWith('group_')) {
setGroupId(key.substring(6));
} else if (key.startsWith('asset_')) {
setGroupId('');
} else {
setGroupId(key);
}
};
let [selectedTags, setSelectedTags] = useState<string[]>([]);
let [groupChooserOpen, setGroupChooserOpen] = useState(false);
let [gatewayChooserOpen, setGatewayChooserOpen] = useState(false);
@@ -608,7 +618,7 @@ const AssetPage = () => {
/* 移动端:垂直布局,树在上,标签过?+ 表格在下 */
<>
<div className="mb-4 bg-white dark:bg-gray-800 rounded-lg">
<AssetTree selected={groupId} onSelect={setGroupId}/>
<AssetTree selected={groupId} onSelect={handleTreeSelect}/>
</div>
{tagFilter}
<ProTable {...tableProps}/>
@@ -621,7 +631,7 @@ const AssetPage = () => {
)}>
<div className="relative rounded-md bg-gray-50 dark:bg-[#141414]">
{!isTreeCollapsed && (
<AssetTree selected={groupId} onSelect={setGroupId}/>
<AssetTree selected={groupId} onSelect={handleTreeSelect}/>
)}
<div
className={cn(
+15 -3
View File
@@ -30,7 +30,7 @@ const AssetTree = ({selected, onSelect}: Props) => {
let [op, setOP] = useState<OP>();
let [expandedKeys, setExpandedKeys] = useState([]);
let [selectedKeys, setSelectedKeys] = useState<React.Key[]>([selected]);
let [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const [theme] = useNTTheme();
let query = useQuery({
@@ -38,6 +38,14 @@ const AssetTree = ({selected, onSelect}: Props) => {
queryFn: assetApi.getGroups,
});
useEffect(() => {
if (selected) {
setSelectedKeys(['group_' + selected]);
} else {
setSelectedKeys([]);
}
}, [selected]);
useEffect(() => {
if (Array.isArray(query.data) && query.data.length > 0) {
setTreeData(query.data);
@@ -197,7 +205,11 @@ const AssetTree = ({selected, onSelect}: Props) => {
danger: true,
icon: <TrashIcon className={'h-4 w-4'}/>,
onClick: () => {
assetApi.deleteGroup(contextMenu.node.key as string).then(() => {
let groupKey = contextMenu.node.key as string;
if (groupKey.startsWith('group_')) {
groupKey = groupKey.substring(6);
}
assetApi.deleteGroup(groupKey).then(() => {
query.refetch();
})
},
@@ -205,7 +217,7 @@ const AssetTree = ({selected, onSelect}: Props) => {
];
const handleRightClick = ({event, node}) => {
if (node.key === 'default') {
if (node.key === 'default' || node.key?.toString().startsWith('asset_')) {
return;
}
// console.log(`handleRightClick`, event, node)
-1
View File
@@ -58,7 +58,6 @@ const DbProxySetting = ({get, set}: SettingProps) => {
<ProFormSwitch
name="db-proxy-enabled"
label={t('db.proxy.enabled')}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
fieldProps={{
+5 -6
View File
@@ -18,14 +18,13 @@ const LogSetting = ({get, set}: SettingProps) => {
style: {display: 'none'}
}
}}>
<ProFormSwitch name="recording-enabled"
<ProFormSwitch name="enable-recording"
label={t('identity.user.recording')}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
<ProFormSelect name="session-saved-limit-days"
<ProFormSelect name="session-saved-limit"
label={t('settings.log.session.saved_limit_days')}
fieldProps={{
options: [
@@ -40,7 +39,7 @@ const LogSetting = ({get, set}: SettingProps) => {
}}
addonAfter={t('general.days')}
/>
<ProFormSelect name="login-log-saved-limit-days"
<ProFormSelect name="login-log-saved-limit"
label={t('settings.log.login_log.saved_limit_days')}
fieldProps={{
options: [
@@ -53,7 +52,7 @@ const LogSetting = ({get, set}: SettingProps) => {
}}
addonAfter={t('general.days')}
/>
<ProFormSelect name="cron-log-saved-limit-days"
<ProFormSelect name="cron-log-saved-limit"
label={t('settings.log.cron_log.saved_limit_days')}
fieldProps={{
options: [
@@ -66,7 +65,7 @@ const LogSetting = ({get, set}: SettingProps) => {
}}
addonAfter={t('general.days')}
/>
<ProFormSelect name="access-log-saved-limit-days"
<ProFormSelect name="access-log-saved-limit"
label={t('settings.log.access_log.saved_limit_days')}
fieldProps={{
options: [
+8 -18
View File
@@ -1,10 +1,8 @@
import React, {useEffect, useState} from 'react';
import {Alert, App, Card, Col, Form, Row, Typography} from "antd";
import React, {useState} from 'react';
import {Alert, App, Card, Col, Row, Typography} from "antd";
import {SettingProps} from "./SettingPage";
import {useQuery} from "@tanstack/react-query";
import {ProForm, ProFormDigit, ProFormSwitch, ProFormText, ProFormTextArea} from "@ant-design/pro-components";
import {useTranslation} from "react-i18next";
import requests from "@/api/core/requests";
import propertyApi from "@/api/property-api";
import {useMobile} from "@/hook/use-mobile";
import {cn} from "@/lib/utils";
@@ -15,22 +13,15 @@ const MailSetting = ({get, set}: SettingProps) => {
const { isMobile } = useMobile();
let {t} = useTranslation();
const [form] = Form.useForm();
let [enabled, setEnabled] = useState(false);
let {message} = App.useApp();
let query = useQuery({
queryKey: ['get-property'],
queryFn: get,
});
useEffect(() => {
if (query.data) {
form.setFieldsValue(query.data);
setEnabled(query.data['mail-enabled']);
}
}, [query.data]);
const wrapGet = async () => {
let values = await get();
setEnabled(values['mail-enabled']);
return values;
}
const handleSendTestMail = async (values: any) => {
await propertyApi.sendMail(values);
@@ -43,14 +34,13 @@ const MailSetting = ({get, set}: SettingProps) => {
<Row gutter={16}>
<Col span={isMobile ? 24 : 12}>
<Card>
<ProForm onFinish={set} request={get} submitter={{
<ProForm onFinish={set} request={wrapGet} submitter={{
resetButtonProps: {
style: {display: 'none'}
}
}}>
<ProFormSwitch name="mail-enabled"
label={t('settings.mail.enabled')}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
fieldProps={{
-8
View File
@@ -19,49 +19,41 @@ const RdpSetting = ({get, set}: SettingProps) => {
}}>
<ProFormSwitch name="enable-wallpaper"
label={t('settings.rdp.enable.wallpaper')}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
<ProFormSwitch name="enable-theming"
label={t("settings.rdp.enable.theming")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
<ProFormSwitch name="enable-font-smoothing"
label={t("settings.rdp.enable.font_smoothing")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
<ProFormSwitch name="enable-full-window-drag"
label={t("settings.rdp.enable.full_window_drag")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
<ProFormSwitch name="enable-desktop-composition"
label={t("settings.rdp.enable.desktop_composition")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
<ProFormSwitch name="enable-menu-animations"
label={t("settings.rdp.enable.menu_animations")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
<ProFormSwitch name="disable-bitmap-caching"
label={t("settings.rdp.disable.bitmap_caching")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
<ProFormSwitch name="disable-offscreen-caching"
label={t("settings.rdp.disable.offscreen_caching")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
@@ -57,14 +57,12 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch
name="login-captcha-enabled"
label={t("settings.security.captcha")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
<ProFormSwitch
name="login-force-totp-enabled"
label={t("settings.security.force_otp")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
@@ -72,7 +70,6 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch
name="disable-password-login"
label={t("settings.security.disable_password_login")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
@@ -82,7 +79,6 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch
name="access-require-mfa"
label={t("settings.security.access_require_mfa")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
@@ -102,7 +98,6 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch
name="login-session-count-custom"
label={t("settings.security.session.count_custom")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
fieldProps={{
@@ -229,7 +224,6 @@ const SecuritySetting = ({get, set}: SettingProps) => {
<ProFormSwitch
name="login-lock-enabled"
label={t("settings.security.login_lock.enabled")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
fieldProps={{
-1
View File
@@ -94,7 +94,6 @@ const SshdSetting = ({get, set}: SettingProps) => {
}}>
<ProFormSwitch name="ssh-server-enabled"
label={t("settings.sshd.enabled")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
fieldProps={{
+3 -17
View File
@@ -1,8 +1,7 @@
import React, {useEffect} from 'react';
import {Form, Typography} from "antd";
import React from 'react';
import {Typography} from "antd";
import {SettingProps} from "./SettingPage";
import {useQuery} from "@tanstack/react-query";
import {ProForm, ProFormDigit, ProFormSelect, ProFormSwitch} from "@ant-design/pro-components";
import {ProForm, ProFormSelect, ProFormSwitch} from "@ant-design/pro-components";
import {useTranslation} from "react-i18next";
const {Title} = Typography;
@@ -10,18 +9,6 @@ const {Title} = Typography;
const VncSetting = ({get, set}: SettingProps) => {
let {t} = useTranslation();
const [form] = Form.useForm();
let query = useQuery({
queryKey: ['get-property'],
queryFn: get,
});
useEffect(() => {
if (query.data) {
form.setFieldsValue(query.data);
}
}, [query.data]);
return (
<div>
@@ -55,7 +42,6 @@ const VncSetting = ({get, set}: SettingProps) => {
/>
<ProFormSwitch name="swap-red-blue"
label={t("settings.vnc.swap_red_blue")}
rules={[{required: true}]}
checkedChildren={t('general.enabled')}
unCheckedChildren={t('general.disabled')}
/>
+1
View File
@@ -198,6 +198,7 @@
}
},
"fs": {
"title": "File Manager",
"operations": {
"batch_download": "Batch Download",
"create_dir": "Create Folder",
+1
View File
@@ -440,6 +440,7 @@
}
},
"fs": {
"title": "文件管理",
"operations": {
"batch_download": "批量下载",
"create_dir": "创建文件夹",