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:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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[]);
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
|
||||
@@ -198,6 +198,7 @@
|
||||
}
|
||||
},
|
||||
"fs": {
|
||||
"title": "File Manager",
|
||||
"operations": {
|
||||
"batch_download": "Batch Download",
|
||||
"create_dir": "Create Folder",
|
||||
|
||||
@@ -440,6 +440,7 @@
|
||||
}
|
||||
},
|
||||
"fs": {
|
||||
"title": "文件管理",
|
||||
"operations": {
|
||||
"batch_download": "批量下载",
|
||||
"create_dir": "创建文件夹",
|
||||
|
||||
Reference in New Issue
Block a user