add audit page

This commit is contained in:
Do Manh Phuong 2026-03-25 12:33:09 +07:00
parent 0d9a1ec002
commit 9df4401c47
20 changed files with 957 additions and 88 deletions

View File

@ -109,7 +109,7 @@ export function CommandActionButtons({ roomName, selectedDevices = [] }: Command
// All rendered commands are sourced from sensitiveCommands — send via sensitive mutation // All rendered commands are sourced from sensitiveCommands — send via sensitive mutation
await executeSensitiveMutation.mutateAsync({ await executeSensitiveMutation.mutateAsync({
roomName, roomName,
command: confirmDialog.command.commandContent, command: confirmDialog.command.commandName,
password: sensitivePassword, password: sensitivePassword,
}); });

View File

@ -0,0 +1,23 @@
import type { Version } from "@/types/file";
import type { ColumnDef } from "@tanstack/react-table";
export const agentColumns: ColumnDef<Version>[] = [
{ accessorKey: "version", header: "Phiên bản" },
{ accessorKey: "fileName", header: "Tên file" },
{
accessorKey: "updatedAt",
header: "Thời gian cập nhật",
cell: ({ getValue }) =>
getValue()
? new Date(getValue() as string).toLocaleString("vi-VN")
: "N/A",
},
{
accessorKey: "requestUpdateAt",
header: "Thời gian yêu cầu cập nhật",
cell: ({ getValue }) =>
getValue()
? new Date(getValue() as string).toLocaleString("vi-VN")
: "N/A",
},
];

View File

@ -0,0 +1,70 @@
// components/columns/apps-column.tsx
import type { Version } from "@/types/file";
import type { ColumnDef } from "@tanstack/react-table";
import { Check, X } from "lucide-react";
// Không gọi hook ở đây — nhận isPending từ ngoài truyền vào
export function createAppsColumns(isPending: boolean): ColumnDef<Version>[] {
return [
{ accessorKey: "version", header: "Phiên bản" },
{ accessorKey: "fileName", header: "Tên file" },
{
accessorKey: "updatedAt",
header: () => (
<div className="whitespace-normal max-w-xs">Thời gian cập nhật</div>
),
cell: ({ getValue }) =>
getValue()
? new Date(getValue() as string).toLocaleString("vi-VN")
: "N/A",
},
{
accessorKey: "requestUpdateAt",
header: () => (
<div className="whitespace-normal max-w-xs">
Thời gian yêu cầu cài đt/tải xuống
</div>
),
cell: ({ getValue }) =>
getValue()
? new Date(getValue() as string).toLocaleString("vi-VN")
: "N/A",
},
{
id: "required",
header: () => (
<div className="whitespace-normal max-w-xs">Đã thêm vào danh sách</div>
),
cell: ({ row }) => {
const isRequired = row.original.isRequired;
return isRequired ? (
<div className="flex items-center gap-1">
<Check className="h-4 w-4 text-green-600" />
<span className="text-sm text-green-600"></span>
</div>
) : (
<div className="flex items-center gap-1">
<X className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-400">Không</span>
</div>
);
},
enableSorting: false,
enableHiding: false,
},
{
id: "select",
header: () => <div className="whitespace-normal max-w-xs">Chọn</div>,
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected?.() ?? false}
onChange={row.getToggleSelectedHandler?.()}
disabled={isPending} // ← nhận từ tham số, không gọi hook
/>
),
enableSorting: false,
enableHiding: false,
},
];
}

View File

@ -0,0 +1,98 @@
import { type ColumnDef } from "@tanstack/react-table";
import { Badge } from "@/components/ui/badge";
import type { Audits } from "@/types/audit";
export const auditColumns: ColumnDef<Audits>[] = [
{
header: "Thời gian",
accessorKey: "dateTime",
cell: ({ getValue }) => {
const v = getValue() as string;
const d = v ? new Date(v) : null;
return d ? (
<div className="text-sm whitespace-nowrap">
<div className="font-medium">{d.toLocaleDateString("vi-VN")}</div>
<div className="text-muted-foreground text-xs">
{d.toLocaleTimeString("vi-VN")}
</div>
</div>
) : (
<span className="text-muted-foreground"></span>
);
},
},
{
header: "User",
accessorKey: "username",
cell: ({ getValue }) => (
<span className="font-medium text-sm whitespace-nowrap">
{getValue() as string}
</span>
),
},
{
header: "Loại",
accessorKey: "apiCall",
cell: ({ getValue }) => {
const v = (getValue() as string) ?? "";
if (!v) return <span className="text-muted-foreground"></span>;
return (
<code className="text-xs bg-muted px-1.5 py-0.5 rounded whitespace-nowrap">
{v}
</code>
);
},
},
{
header: "Hành động",
accessorKey: "action",
cell: ({ getValue }) => (
<code className="text-xs bg-muted px-1.5 py-0.5 rounded whitespace-nowrap">
{getValue() as string}
</code>
),
},
{
header: "URL",
accessorKey: "url",
cell: ({ getValue }) => (
<code className="text-xs text-muted-foreground max-w-[180px] truncate block">
{(getValue() as string) ?? "—"}
</code>
),
},
{
header: "Kết quả",
accessorKey: "isSuccess",
cell: ({ getValue }) => {
const v = getValue();
if (v == null) return <span className="text-muted-foreground"></span>;
return v ? (
<Badge variant="outline" className="text-green-600 border-green-600 whitespace-nowrap">
Thành công
</Badge>
) : (
<Badge variant="outline" className="text-red-600 border-red-600 whitespace-nowrap">
Thất bại
</Badge>
);
},
},
{
header: "Nội dung request",
accessorKey: "requestPayload",
cell: ({ getValue }) => {
const v = getValue() as string;
if (!v) return <span className="text-muted-foreground"></span>;
let preview = v;
try {
preview = JSON.stringify(JSON.parse(v));
} catch {}
return (
<span className="text-xs text-muted-foreground max-w-[200px] truncate block">
{preview}
</span>
);
},
},
];

View File

@ -0,0 +1,180 @@
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Separator } from "@/components/ui/separator";
import type { Audits } from "@/types/audit";
function JsonDisplay({ value }: { value: string | null | undefined }) {
if (!value) return <span className="text-muted-foreground"></span>;
try {
return (
<pre className="text-xs bg-muted/60 p-2.5 rounded-md overflow-auto whitespace-pre-wrap break-all leading-relaxed max-h-48 font-mono">
{JSON.stringify(JSON.parse(value), null, 2)}
</pre>
);
} catch {
return <span className="text-xs break-all font-mono">{value}</span>;
}
}
interface AuditDetailDialogProps {
audit: Audits | null;
open: boolean;
onClose: () => void;
}
export function AuditDetailDialog({
audit,
open,
onClose,
}: AuditDetailDialogProps) {
if (!audit) return null;
return (
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="max-w-2xl w-full max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
Chi tiết audit
<span className="text-muted-foreground font-normal text-sm">
#{audit.id}
</span>
</DialogTitle>
</DialogHeader>
<Separator />
<div className="grid grid-cols-2 gap-x-6 gap-y-3 pt-1">
<div className="space-y-0.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Thời gian
</p>
<p className="text-sm font-medium">
{audit.dateTime
? new Date(audit.dateTime).toLocaleString("vi-VN")
: "—"}
</p>
</div>
<div className="space-y-0.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
User
</p>
<p className="text-sm font-medium">{audit.username}</p>
</div>
<div className="space-y-0.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
API Call
</p>
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
{audit.apiCall ?? "—"}
</code>
</div>
<div className="space-y-0.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Kết quả
</p>
<div>
{audit.isSuccess == null ? (
<span className="text-muted-foreground text-sm"></span>
) : audit.isSuccess ? (
<Badge
variant="outline"
className="text-green-600 border-green-600"
>
Thành công
</Badge>
) : (
<Badge
variant="outline"
className="text-red-600 border-red-600"
>
Thất bại
</Badge>
)}
</div>
</div>
<div className="space-y-0.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Hành đng
</p>
<code className="text-xs bg-muted px-1.5 py-0.5 rounded">
{audit.action}
</code>
</div>
<div className="space-y-0.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
URL
</p>
<code className="text-xs text-muted-foreground break-all">
{audit.url ?? "—"}
</code>
</div>
<div className="space-y-0.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Bảng
</p>
<p className="text-sm">{audit.tableName ?? "—"}</p>
</div>
<div className="space-y-0.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Entity ID
</p>
<p className="text-sm">{audit.entityId ?? "—"}</p>
</div>
<div className="col-span-2 space-y-0.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Lỗi
</p>
<p className="text-sm text-red-600">{audit.errorMessage ?? "—"}</p>
</div>
</div>
<Separator />
<div className="space-y-4">
<div className="space-y-1.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Nội dung request
</p>
<JsonDisplay value={audit.requestPayload} />
</div>
<div className="space-y-1.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Giá trị
</p>
<JsonDisplay value={audit.oldValues} />
</div>
<div className="space-y-1.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Giá trị mới
</p>
<JsonDisplay value={audit.newValues} />
</div>
<div className="space-y-1.5">
<p className="text-xs text-muted-foreground uppercase tracking-wide">
Kết quả
</p>
<p className="text-sm">{audit.isSuccess == null ? "—" : audit.isSuccess ? "Thành công" : "Thất bại"}</p>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@ -0,0 +1,73 @@
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
interface AuditFilterBarProps {
username: string | null;
action: string | null;
from: string | null;
to: string | null;
isLoading: boolean;
isFetching: boolean;
onUsernameChange: (v: string | null) => void;
onActionChange: (v: string | null) => void;
onFromChange: (v: string | null) => void;
onToChange: (v: string | null) => void;
onSearch: () => void;
onReset: () => void;
}
export function AuditFilterBar({
username,
action,
from,
to,
isLoading,
isFetching,
onUsernameChange,
onActionChange,
onFromChange,
onToChange,
onSearch,
onReset,
}: AuditFilterBarProps) {
return (
<div className="flex gap-2 mb-4 flex-wrap items-end">
<Input
className="w-36"
placeholder="Username"
value={username ?? ""}
onChange={(e) => onUsernameChange(e.target.value || null)}
/>
<Input
className="w-44"
placeholder="Hành động..."
value={action ?? ""}
onChange={(e) => onActionChange(e.target.value || null)}
/>
<Input
className="w-36"
type="date"
value={from ?? ""}
onChange={(e) => onFromChange(e.target.value || null)}
/>
<Input
className="w-36"
type="date"
value={to ?? ""}
onChange={(e) => onToChange(e.target.value || null)}
/>
<div className="flex gap-2">
<Button onClick={onSearch} disabled={isFetching || isLoading} size="sm">
Tìm
</Button>
<Button variant="outline" onClick={onReset} size="sm">
Reset
</Button>
</div>
</div>
);
}

View File

@ -82,4 +82,12 @@ export const API_ENDPOINTS = {
TOGGLE_PERMISSION: (roleId: number, permissionId: number) => TOGGLE_PERMISSION: (roleId: number, permissionId: number) =>
`${BASE_URL}/Role/${roleId}/permissions/${permissionId}/toggle`, `${BASE_URL}/Role/${roleId}/permissions/${permissionId}/toggle`,
}, },
DASHBOARD:
{
}
,
AUDIT: {
GET_AUDITS: `${BASE_URL}/Audit/audits`,
}
}; };

View File

@ -10,6 +10,9 @@ export * from "./useDeviceCommQueries";
// Command Queries // Command Queries
export * from "./useCommandQueries"; export * from "./useCommandQueries";
// Audit Queries
export * from "./useAuditQueries";
// Permission Queries // Permission Queries
export * from "./usePermissionQueries"; export * from "./usePermissionQueries";

View File

@ -0,0 +1,37 @@
import { useQuery } from "@tanstack/react-query";
import * as auditService from "@/services/audit.service";
import type { PageResult, Audits } from "@/types/audit";
const AUDIT_QUERY_KEYS = {
all: ["audit"] as const,
list: () => [...AUDIT_QUERY_KEYS.all, "list"] as const,
audits: (params: any) => [...AUDIT_QUERY_KEYS.all, "audits", params] as const,
};
export function useGetAudits(
params: {
pageNumber?: number;
pageSize?: number;
username?: string | null;
action?: string | null;
from?: string | null;
to?: string | null;
} = { pageNumber: 1, pageSize: 20 },
enabled = true
) {
const { pageNumber = 1, pageSize = 20, username, action, from, to } = params;
return useQuery<PageResult<Audits>>({
queryKey: AUDIT_QUERY_KEYS.audits({ pageNumber, pageSize, username, action, from, to }),
queryFn: () =>
auditService.getAudits(
pageNumber,
pageSize,
username ?? null,
action ?? null,
from ?? null,
to ?? null
),
enabled,
});
}

View File

@ -18,6 +18,7 @@ import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index
import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index' import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index'
import { Route as AuthCommandsIndexRouteImport } from './routes/_auth/commands/index' import { Route as AuthCommandsIndexRouteImport } from './routes/_auth/commands/index'
import { Route as AuthBlacklistsIndexRouteImport } from './routes/_auth/blacklists/index' import { Route as AuthBlacklistsIndexRouteImport } from './routes/_auth/blacklists/index'
import { Route as AuthAuditsIndexRouteImport } from './routes/_auth/audits/index'
import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index' import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index'
import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index' import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index'
import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index' import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index'
@ -29,6 +30,7 @@ import { Route as AuthProfileUserNameIndexRouteImport } from './routes/_auth/pro
import { Route as AuthUserRoleRoleIdIndexRouteImport } from './routes/_auth/user/role/$roleId/index' import { Route as AuthUserRoleRoleIdIndexRouteImport } from './routes/_auth/user/role/$roleId/index'
import { Route as AuthUserChangePasswordUserNameIndexRouteImport } from './routes/_auth/user/change-password/$userName/index' import { Route as AuthUserChangePasswordUserNameIndexRouteImport } from './routes/_auth/user/change-password/$userName/index'
import { Route as AuthRoomsRoomNameFolderStatusIndexRouteImport } from './routes/_auth/rooms/$roomName/folder-status/index' import { Route as AuthRoomsRoomNameFolderStatusIndexRouteImport } from './routes/_auth/rooms/$roomName/folder-status/index'
import { Route as AuthRoomsRoomNameConnectIndexRouteImport } from './routes/_auth/rooms/$roomName/connect/index'
import { Route as AuthRoleIdEditIndexRouteImport } from './routes/_auth/role/$id/edit/index' import { Route as AuthRoleIdEditIndexRouteImport } from './routes/_auth/role/$id/edit/index'
const AuthRoute = AuthRouteImport.update({ const AuthRoute = AuthRouteImport.update({
@ -75,6 +77,11 @@ const AuthBlacklistsIndexRoute = AuthBlacklistsIndexRouteImport.update({
path: '/blacklists/', path: '/blacklists/',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
const AuthAuditsIndexRoute = AuthAuditsIndexRouteImport.update({
id: '/audits/',
path: '/audits/',
getParentRoute: () => AuthRoute,
} as any)
const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({ const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({
id: '/apps/', id: '/apps/',
path: '/apps/', path: '/apps/',
@ -134,6 +141,12 @@ const AuthRoomsRoomNameFolderStatusIndexRoute =
path: '/rooms/$roomName/folder-status/', path: '/rooms/$roomName/folder-status/',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
const AuthRoomsRoomNameConnectIndexRoute =
AuthRoomsRoomNameConnectIndexRouteImport.update({
id: '/rooms/$roomName/connect/',
path: '/rooms/$roomName/connect/',
getParentRoute: () => AuthRoute,
} as any)
const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({ const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({
id: '/role/$id/edit/', id: '/role/$id/edit/',
path: '/role/$id/edit/', path: '/role/$id/edit/',
@ -145,6 +158,7 @@ export interface FileRoutesByFullPath {
'/login': typeof authLoginIndexRoute '/login': typeof authLoginIndexRoute
'/agent': typeof AuthAgentIndexRoute '/agent': typeof AuthAgentIndexRoute
'/apps': typeof AuthAppsIndexRoute '/apps': typeof AuthAppsIndexRoute
'/audits': typeof AuthAuditsIndexRoute
'/blacklists': typeof AuthBlacklistsIndexRoute '/blacklists': typeof AuthBlacklistsIndexRoute
'/commands': typeof AuthCommandsIndexRoute '/commands': typeof AuthCommandsIndexRoute
'/dashboard': typeof AuthDashboardIndexRoute '/dashboard': typeof AuthDashboardIndexRoute
@ -158,6 +172,7 @@ export interface FileRoutesByFullPath {
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute '/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
'/user/create': typeof AuthUserCreateIndexRoute '/user/create': typeof AuthUserCreateIndexRoute
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute '/role/$id/edit': typeof AuthRoleIdEditIndexRoute
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute '/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute '/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute '/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
@ -167,6 +182,7 @@ export interface FileRoutesByTo {
'/login': typeof authLoginIndexRoute '/login': typeof authLoginIndexRoute
'/agent': typeof AuthAgentIndexRoute '/agent': typeof AuthAgentIndexRoute
'/apps': typeof AuthAppsIndexRoute '/apps': typeof AuthAppsIndexRoute
'/audits': typeof AuthAuditsIndexRoute
'/blacklists': typeof AuthBlacklistsIndexRoute '/blacklists': typeof AuthBlacklistsIndexRoute
'/commands': typeof AuthCommandsIndexRoute '/commands': typeof AuthCommandsIndexRoute
'/dashboard': typeof AuthDashboardIndexRoute '/dashboard': typeof AuthDashboardIndexRoute
@ -180,6 +196,7 @@ export interface FileRoutesByTo {
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute '/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
'/user/create': typeof AuthUserCreateIndexRoute '/user/create': typeof AuthUserCreateIndexRoute
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute '/role/$id/edit': typeof AuthRoleIdEditIndexRoute
'/rooms/$roomName/connect': typeof AuthRoomsRoomNameConnectIndexRoute
'/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute '/rooms/$roomName/folder-status': typeof AuthRoomsRoomNameFolderStatusIndexRoute
'/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute '/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute
'/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute '/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute
@ -191,6 +208,7 @@ export interface FileRoutesById {
'/(auth)/login/': typeof authLoginIndexRoute '/(auth)/login/': typeof authLoginIndexRoute
'/_auth/agent/': typeof AuthAgentIndexRoute '/_auth/agent/': typeof AuthAgentIndexRoute
'/_auth/apps/': typeof AuthAppsIndexRoute '/_auth/apps/': typeof AuthAppsIndexRoute
'/_auth/audits/': typeof AuthAuditsIndexRoute
'/_auth/blacklists/': typeof AuthBlacklistsIndexRoute '/_auth/blacklists/': typeof AuthBlacklistsIndexRoute
'/_auth/commands/': typeof AuthCommandsIndexRoute '/_auth/commands/': typeof AuthCommandsIndexRoute
'/_auth/dashboard/': typeof AuthDashboardIndexRoute '/_auth/dashboard/': typeof AuthDashboardIndexRoute
@ -204,6 +222,7 @@ export interface FileRoutesById {
'/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute '/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute
'/_auth/user/create/': typeof AuthUserCreateIndexRoute '/_auth/user/create/': typeof AuthUserCreateIndexRoute
'/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute '/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute
'/_auth/rooms/$roomName/connect/': typeof AuthRoomsRoomNameConnectIndexRoute
'/_auth/rooms/$roomName/folder-status/': typeof AuthRoomsRoomNameFolderStatusIndexRoute '/_auth/rooms/$roomName/folder-status/': typeof AuthRoomsRoomNameFolderStatusIndexRoute
'/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute '/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute
'/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute '/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute
@ -215,6 +234,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/agent' | '/agent'
| '/apps' | '/apps'
| '/audits'
| '/blacklists' | '/blacklists'
| '/commands' | '/commands'
| '/dashboard' | '/dashboard'
@ -228,6 +248,7 @@ export interface FileRouteTypes {
| '/rooms/$roomName' | '/rooms/$roomName'
| '/user/create' | '/user/create'
| '/role/$id/edit' | '/role/$id/edit'
| '/rooms/$roomName/connect'
| '/rooms/$roomName/folder-status' | '/rooms/$roomName/folder-status'
| '/user/change-password/$userName' | '/user/change-password/$userName'
| '/user/role/$roleId' | '/user/role/$roleId'
@ -237,6 +258,7 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/agent' | '/agent'
| '/apps' | '/apps'
| '/audits'
| '/blacklists' | '/blacklists'
| '/commands' | '/commands'
| '/dashboard' | '/dashboard'
@ -250,6 +272,7 @@ export interface FileRouteTypes {
| '/rooms/$roomName' | '/rooms/$roomName'
| '/user/create' | '/user/create'
| '/role/$id/edit' | '/role/$id/edit'
| '/rooms/$roomName/connect'
| '/rooms/$roomName/folder-status' | '/rooms/$roomName/folder-status'
| '/user/change-password/$userName' | '/user/change-password/$userName'
| '/user/role/$roleId' | '/user/role/$roleId'
@ -260,6 +283,7 @@ export interface FileRouteTypes {
| '/(auth)/login/' | '/(auth)/login/'
| '/_auth/agent/' | '/_auth/agent/'
| '/_auth/apps/' | '/_auth/apps/'
| '/_auth/audits/'
| '/_auth/blacklists/' | '/_auth/blacklists/'
| '/_auth/commands/' | '/_auth/commands/'
| '/_auth/dashboard/' | '/_auth/dashboard/'
@ -273,6 +297,7 @@ export interface FileRouteTypes {
| '/_auth/rooms/$roomName/' | '/_auth/rooms/$roomName/'
| '/_auth/user/create/' | '/_auth/user/create/'
| '/_auth/role/$id/edit/' | '/_auth/role/$id/edit/'
| '/_auth/rooms/$roomName/connect/'
| '/_auth/rooms/$roomName/folder-status/' | '/_auth/rooms/$roomName/folder-status/'
| '/_auth/user/change-password/$userName/' | '/_auth/user/change-password/$userName/'
| '/_auth/user/role/$roleId/' | '/_auth/user/role/$roleId/'
@ -349,6 +374,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthBlacklistsIndexRouteImport preLoaderRoute: typeof AuthBlacklistsIndexRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
'/_auth/audits/': {
id: '/_auth/audits/'
path: '/audits'
fullPath: '/audits'
preLoaderRoute: typeof AuthAuditsIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/apps/': { '/_auth/apps/': {
id: '/_auth/apps/' id: '/_auth/apps/'
path: '/apps' path: '/apps'
@ -426,6 +458,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthRoomsRoomNameFolderStatusIndexRouteImport preLoaderRoute: typeof AuthRoomsRoomNameFolderStatusIndexRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
'/_auth/rooms/$roomName/connect/': {
id: '/_auth/rooms/$roomName/connect/'
path: '/rooms/$roomName/connect'
fullPath: '/rooms/$roomName/connect'
preLoaderRoute: typeof AuthRoomsRoomNameConnectIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/role/$id/edit/': { '/_auth/role/$id/edit/': {
id: '/_auth/role/$id/edit/' id: '/_auth/role/$id/edit/'
path: '/role/$id/edit' path: '/role/$id/edit'
@ -439,6 +478,7 @@ declare module '@tanstack/react-router' {
interface AuthRouteChildren { interface AuthRouteChildren {
AuthAgentIndexRoute: typeof AuthAgentIndexRoute AuthAgentIndexRoute: typeof AuthAgentIndexRoute
AuthAppsIndexRoute: typeof AuthAppsIndexRoute AuthAppsIndexRoute: typeof AuthAppsIndexRoute
AuthAuditsIndexRoute: typeof AuthAuditsIndexRoute
AuthBlacklistsIndexRoute: typeof AuthBlacklistsIndexRoute AuthBlacklistsIndexRoute: typeof AuthBlacklistsIndexRoute
AuthCommandsIndexRoute: typeof AuthCommandsIndexRoute AuthCommandsIndexRoute: typeof AuthCommandsIndexRoute
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute
@ -452,6 +492,7 @@ interface AuthRouteChildren {
AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute
AuthUserCreateIndexRoute: typeof AuthUserCreateIndexRoute AuthUserCreateIndexRoute: typeof AuthUserCreateIndexRoute
AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute
AuthRoomsRoomNameConnectIndexRoute: typeof AuthRoomsRoomNameConnectIndexRoute
AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute AuthRoomsRoomNameFolderStatusIndexRoute: typeof AuthRoomsRoomNameFolderStatusIndexRoute
AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute
AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute
@ -460,6 +501,7 @@ interface AuthRouteChildren {
const AuthRouteChildren: AuthRouteChildren = { const AuthRouteChildren: AuthRouteChildren = {
AuthAgentIndexRoute: AuthAgentIndexRoute, AuthAgentIndexRoute: AuthAgentIndexRoute,
AuthAppsIndexRoute: AuthAppsIndexRoute, AuthAppsIndexRoute: AuthAppsIndexRoute,
AuthAuditsIndexRoute: AuthAuditsIndexRoute,
AuthBlacklistsIndexRoute: AuthBlacklistsIndexRoute, AuthBlacklistsIndexRoute: AuthBlacklistsIndexRoute,
AuthCommandsIndexRoute: AuthCommandsIndexRoute, AuthCommandsIndexRoute: AuthCommandsIndexRoute,
AuthDashboardIndexRoute: AuthDashboardIndexRoute, AuthDashboardIndexRoute: AuthDashboardIndexRoute,
@ -473,6 +515,7 @@ const AuthRouteChildren: AuthRouteChildren = {
AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute, AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute,
AuthUserCreateIndexRoute: AuthUserCreateIndexRoute, AuthUserCreateIndexRoute: AuthUserCreateIndexRoute,
AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute, AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute,
AuthRoomsRoomNameConnectIndexRoute: AuthRoomsRoomNameConnectIndexRoute,
AuthRoomsRoomNameFolderStatusIndexRoute: AuthRoomsRoomNameFolderStatusIndexRoute:
AuthRoomsRoomNameFolderStatusIndexRoute, AuthRoomsRoomNameFolderStatusIndexRoute,
AuthUserChangePasswordUserNameIndexRoute: AuthUserChangePasswordUserNameIndexRoute:

View File

@ -7,11 +7,10 @@ import {
useUpdateAgent, useUpdateAgent,
} from "@/hooks/queries"; } from "@/hooks/queries";
import { toast } from "sonner"; import { toast } from "sonner";
import type { ColumnDef } from "@tanstack/react-table";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
import type { Version } from "@/types/file"; import type { Version } from "@/types/file";
import { ErrorFetchingPage } from "@/components/pages/error-fetching-page"; import { ErrorFetchingPage } from "@/components/pages/error-fetching-page";
import { agentColumns } from "@/components/columns/agent-column";
export const Route = createFileRoute("/_auth/agent/")({ export const Route = createFileRoute("/_auth/agent/")({
head: () => ({ meta: [{ title: "Quản lý Agent" }] }), head: () => ({ meta: [{ title: "Quản lý Agent" }] }),
component: AgentsPage, component: AgentsPage,
@ -71,26 +70,7 @@ function AgentsPage() {
}; };
// Cột bảng // Cột bảng
const columns: ColumnDef<Version>[] = [
{ accessorKey: "version", header: "Phiên bản" },
{ accessorKey: "fileName", header: "Tên file" },
{
accessorKey: "updatedAt",
header: "Thời gian cập nhật",
cell: ({ getValue }) =>
getValue()
? new Date(getValue() as string).toLocaleString("vi-VN")
: "N/A",
},
{
accessorKey: "requestUpdateAt",
header: "Thời gian yêu cầu cập nhật",
cell: ({ getValue }) =>
getValue()
? new Date(getValue() as string).toLocaleString("vi-VN")
: "N/A",
},
];
return ( return (
<AppManagerTemplate<Version> <AppManagerTemplate<Version>
@ -98,7 +78,7 @@ function AgentsPage() {
description="Quản lý và theo dõi các phiên bản Agent" description="Quản lý và theo dõi các phiên bản Agent"
data={versionList} data={versionList}
isLoading={isLoading} isLoading={isLoading}
columns={columns} columns={agentColumns}
onUpload={handleUpload} onUpload={handleUpload}
onUpdate={handleUpdate} onUpdate={handleUpdate}
updateLoading={updateMutation.isPending} updateLoading={updateMutation.isPending}

View File

@ -11,12 +11,10 @@ import {
useDownloadFiles, useDownloadFiles,
} from "@/hooks/queries"; } from "@/hooks/queries";
import { toast } from "sonner"; import { toast } from "sonner";
import type { ColumnDef } from "@tanstack/react-table";
import type { AxiosProgressEvent } from "axios"; import type { AxiosProgressEvent } from "axios";
import type { Version } from "@/types/file"; import type { Version } from "@/types/file";
import { Check, X } from "lucide-react"; import { useMemo, useState } from "react";
import { useState } from "react"; import { createAppsColumns } from "@/components/columns/apps-column";
export const Route = createFileRoute("/_auth/apps/")({ export const Route = createFileRoute("/_auth/apps/")({
head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }), head: () => ({ meta: [{ title: "Quản lý phần mềm" }] }),
component: AppsComponent, component: AppsComponent,
@ -51,62 +49,10 @@ function AppsComponent() {
const deleteRequiredFileMutation = useDeleteRequiredFile(); const deleteRequiredFileMutation = useDeleteRequiredFile();
// Cột bảng const columns = useMemo(
const columns: ColumnDef<Version>[] = [ () => createAppsColumns(installMutation.isPending),
{ accessorKey: "version", header: "Phiên bản" }, [installMutation.isPending]
{ accessorKey: "fileName", header: "Tên file" }, );
{
accessorKey: "updatedAt",
header: () => <div className="whitespace-normal max-w-xs">Thời gian cập nhật</div>,
cell: ({ getValue }) =>
getValue()
? new Date(getValue() as string).toLocaleString("vi-VN")
: "N/A",
},
{
accessorKey: "requestUpdateAt",
header: () => <div className="whitespace-normal max-w-xs">Thời gian yêu cầu cài đt/tải xuống</div>,
cell: ({ getValue }) =>
getValue()
? new Date(getValue() as string).toLocaleString("vi-VN")
: "N/A",
},
{
id: "required",
header: () => <div className="whitespace-normal max-w-xs">Đã thêm vào danh sách</div>,
cell: ({ row }) => {
const isRequired = row.original.isRequired;
return isRequired ? (
<div className="flex items-center gap-1">
<Check className="h-4 w-4 text-green-600" />
<span className="text-sm text-green-600"></span>
</div>
) : (
<div className="flex items-center gap-1">
<X className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-400">Không</span>
</div>
);
},
enableSorting: false,
enableHiding: false,
},
{
id: "select",
header: () => <div className="whitespace-normal max-w-xs">Chọn</div>,
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected?.() ?? false}
onChange={row.getToggleSelectedHandler?.()}
disabled={installMutation.isPending}
/>
),
enableSorting: false,
enableHiding: false,
},
];
// Upload file MSI // Upload file MSI
const handleUpload = async ( const handleUpload = async (
fd: FormData, fd: FormData,

View File

@ -0,0 +1,92 @@
import { createFileRoute } from "@tanstack/react-router";
import { useState, useEffect } from "react";
import { useGetAudits } from "@/hooks/queries";
import type { Audits } from "@/types/audit";
import { AuditListTemplate } from "@/template/audit-list-template";
import { auditColumns } from "@/components/columns/audit-column";
export const Route = createFileRoute("/_auth/audits/")({
head: () => ({ meta: [{ title: "Audit Logs" }] }),
loader: async ({ context }) => {
context.breadcrumbs = [{ title: "Audit logs", path: "#" }];
},
component: AuditsPage,
});
function AuditsPage() {
const [pageNumber, setPageNumber] = useState(1);
const [pageSize] = useState(20);
const [username, setUsername] = useState<string | null>(null);
const [action, setAction] = useState<string | null>(null);
const [from, setFrom] = useState<string | null>(null);
const [to, setTo] = useState<string | null>(null);
const [selectedAudit, setSelectedAudit] = useState<Audits | null>(null);
const { data, isLoading, refetch, isFetching } = useGetAudits(
{
pageNumber,
pageSize,
username,
action,
from,
to,
},
true
) as any;
const items: Audits[] = data?.items ?? [];
const total: number = data?.totalCount ?? 0;
const pageCount = Math.max(1, Math.ceil(total / pageSize));
useEffect(() => {
refetch();
}, [pageNumber, pageSize]);
const handleSearch = () => {
setPageNumber(1);
refetch();
};
const handleReset = () => {
setUsername(null);
setAction(null);
setFrom(null);
setTo(null);
setPageNumber(1);
refetch();
};
return (
<AuditListTemplate
// data
items={items}
total={total}
columns={auditColumns}
isLoading={isLoading}
isFetching={isFetching}
// pagination
pageNumber={pageNumber}
pageSize={pageSize}
pageCount={pageCount}
canPreviousPage={pageNumber > 1}
canNextPage={pageNumber < pageCount}
onPreviousPage={() => setPageNumber((p) => Math.max(1, p - 1))}
onNextPage={() => setPageNumber((p) => Math.min(pageCount, p + 1))}
// filter
username={username}
action={action}
from={from}
to={to}
onUsernameChange={setUsername}
onActionChange={setAction}
onFromChange={setFrom}
onToChange={setTo}
onSearch={handleSearch}
onReset={handleReset}
// detail dialog
selectedAudit={selectedAudit}
onRowClick={setSelectedAudit}
onDialogClose={() => setSelectedAudit(null)}
/>
);
}

View File

@ -11,7 +11,6 @@ import {
useSendCommand, useSendCommand,
} from "@/hooks/queries"; } from "@/hooks/queries";
import { toast } from "sonner"; import { toast } from "sonner";
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { Check, X, Edit2, Trash2 } from "lucide-react"; import { Check, X, Edit2, Trash2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";

View File

@ -0,0 +1,9 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_auth/rooms/$roomName/connect/')({
component: RouteComponent,
})
function RouteComponent() {
return <div>Hello "/_auth/rooms/$roomName/connect/"!</div>
}

View File

@ -0,0 +1,19 @@
import axios from "@/config/axios";
import { API_ENDPOINTS } from "@/config/api";
import type { PageResult, Audits } from "@/types/audit";
export async function getAudits(
pageNumber = 1,
pageSize = 20,
username?: string | null,
action?: string | null,
from?: string | null,
to?: string | null
): Promise<PageResult<Audits>> {
const response = await axios.get<PageResult<Audits>>(API_ENDPOINTS.AUDIT.GET_AUDITS, {
params: { pageNumber, pageSize, username, action, from, to },
});
// API trả về camelCase khớp với PageResult<Audits> — dùng trực tiếp, không cần map
return response.data;
}

View File

@ -0,0 +1,242 @@
import {
type ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import {
Table,
TableHeader,
TableBody,
TableRow,
TableHead,
TableCell,
} from "@/components/ui/table";
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { type Audits } from "@/types/audit";
import { AuditFilterBar } from "@/components/filters/audit-filter-bar";
import { AuditDetailDialog } from "@/components/dialogs/audit-detail-dialog";
interface AuditListTemplateProps {
// data
items: Audits[];
total: number;
columns: ColumnDef<Audits>[];
isLoading: boolean;
isFetching: boolean;
// pagination
pageNumber: number;
pageSize: number;
pageCount: number;
onPreviousPage: () => void;
onNextPage: () => void;
canPreviousPage: boolean;
canNextPage: boolean;
// filter
username: string | null;
action: string | null;
from: string | null;
to: string | null;
onUsernameChange: (v: string | null) => void;
onActionChange: (v: string | null) => void;
onFromChange: (v: string | null) => void;
onToChange: (v: string | null) => void;
onSearch: () => void;
onReset: () => void;
// detail dialog
selectedAudit: Audits | null;
onRowClick: (audit: Audits) => void;
onDialogClose: () => void;
}
export function AuditListTemplate({
items,
total,
columns,
isLoading,
isFetching,
pageNumber,
pageSize,
pageCount,
onPreviousPage,
onNextPage,
canPreviousPage,
canNextPage,
username,
action,
from,
to,
onUsernameChange,
onActionChange,
onFromChange,
onToChange,
onSearch,
onReset,
selectedAudit,
onRowClick,
onDialogClose,
}: AuditListTemplateProps) {
const table = useReactTable({
data: items,
columns,
state: {
pagination: { pageIndex: Math.max(0, pageNumber - 1), pageSize },
},
pageCount,
manualPagination: true,
onPaginationChange: (updater) => {
const next =
typeof updater === "function"
? updater({ pageIndex: Math.max(0, pageNumber - 1), pageSize })
: updater;
const newPage = (next.pageIndex ?? 0) + 1;
if (newPage > pageNumber) onNextPage();
else if (newPage < pageNumber) onPreviousPage();
},
getCoreRowModel: getCoreRowModel(),
});
return (
<div className="w-full px-4 md:px-6 space-y-4">
<div>
<h1 className="text-2xl md:text-3xl font-bold">Nhật hoạt đng</h1>
<p className="text-muted-foreground mt-1 text-sm">
Xem nhật audit hệ thống
</p>
</div>
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-base">Danh sách audit</CardTitle>
<CardDescription className="text-xs">
Lọc theo người dùng, loại, hành đng khoảng thời gian. Nhấn vào
dòng đ xem chi tiết.
</CardDescription>
</CardHeader>
<CardContent>
<AuditFilterBar
username={username}
action={action}
from={from}
to={to}
isLoading={isLoading}
isFetching={isFetching}
onUsernameChange={onUsernameChange}
onActionChange={onActionChange}
onFromChange={onFromChange}
onToChange={onToChange}
onSearch={onSearch}
onReset={onReset}
/>
<div className="rounded-md border overflow-x-auto">
<Table className="min-w-[640px] w-full">
<TableHeader className="sticky top-0 bg-background z-10">
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id}>
{hg.headers.map((header) => (
<TableHead
key={header.id}
className="text-xs font-semibold whitespace-nowrap"
>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{isLoading || isFetching ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-10 text-muted-foreground text-sm"
>
Đang tải...
</TableCell>
</TableRow>
) : table.getRowModel().rows.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center py-10 text-muted-foreground text-sm"
>
Không dữ liệu
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className="hover:bg-muted/40 cursor-pointer"
onClick={() => onRowClick(row.original)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-2.5 align-middle">
{cell.column.columnDef.cell
? flexRender(
cell.column.columnDef.cell,
cell.getContext()
)
: String(cell.getValue() ?? "")}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-between mt-4">
<span className="text-xs text-muted-foreground">
Hiển thị {items.length} / {total} mục
</span>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={!canPreviousPage || isFetching}
onClick={onPreviousPage}
>
Trước
</Button>
<span className="text-sm tabular-nums">
{pageNumber} / {pageCount}
</span>
<Button
variant="outline"
size="sm"
disabled={!canNextPage || isFetching}
onClick={onNextPage}
>
Sau
</Button>
</div>
</div>
</CardContent>
</Card>
<AuditDetailDialog
audit={selectedAudit}
open={!!selectedAudit}
onClose={onDialogClose}
/>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { AppWindow, Building, CircleX, Folder, Home, ShieldCheck, Terminal, UserPlus} from "lucide-react"; import { AppWindow, Building, CircleX, ClipboardList, Folder, Home, ShieldCheck, Terminal, UserPlus} from "lucide-react";
import { PermissionEnum } from "./permission"; import { PermissionEnum } from "./permission";
enum AppSidebarSectionCode { enum AppSidebarSectionCode {
@ -93,6 +93,17 @@ export const appSidebarSection = {
permissions: [PermissionEnum.VIEW_USER], permissions: [PermissionEnum.VIEW_USER],
} }
] ]
},
{
title: "Audits",
items: [
{
title: "Lịch sử hoạt động",
url: "/audits",
icon: ClipboardList,
permissions: [PermissionEnum.VIEW_AUDIT_LOGS],
}
]
} }
], ],
}; };

29
src/types/audit.ts Normal file
View File

@ -0,0 +1,29 @@
export interface Audits {
id: number;
username: string;
dateTime: string; // ISO string
// request identity
apiCall?: string; // Controller.ActionName
url?: string;
requestPayload?: string; // request body (redacted)
// DB fields — null if request didn't touch DB
action?: string;
tableName?: string;
entityId?: string;
oldValues?: string;
newValues?: string;
// result
isSuccess?: boolean;
errorMessage?: string;
}
export interface PageResult<T> {
items: T[];
totalCount: number;
pageNumber: number;
pageSize: number;
totalPages: number;
}

View File

@ -44,6 +44,7 @@ export enum PermissionEnum {
EDIT_COMMAND = 53, EDIT_COMMAND = 53,
DEL_COMMAND = 54, DEL_COMMAND = 54,
SEND_COMMAND = 55, SEND_COMMAND = 55,
SEND_SENSITIVE_COMMAND = 56,
//DEVICE_OPERATION //DEVICE_OPERATION
DEVICE_OPERATION = 70, DEVICE_OPERATION = 70,
@ -59,10 +60,12 @@ export enum PermissionEnum {
VIEW_ACCOUNT_ROOM = 115, VIEW_ACCOUNT_ROOM = 115,
EDIT_ACCOUNT_ROOM = 116, EDIT_ACCOUNT_ROOM = 116,
//WARNING_OPERATION //WARNING_OPERATION
WARNING_OPERATION = 140, WARNING_OPERATION = 140,
VIEW_WARNING = 141, VIEW_WARNING = 141,
//USER_OPERATION //USER_OPERATION
USER_OPERATION = 150, USER_OPERATION = 150,
VIEW_USER_ROLE = 151, VIEW_USER_ROLE = 151,
@ -80,7 +83,7 @@ export enum PermissionEnum {
DEL_ROLE = 164, DEL_ROLE = 164,
// AGENT // AGENT
APP_OPERATION = 170, AGENT_OPERATION = 170,
VIEW_AGENT = 171, VIEW_AGENT = 171,
UPDATE_AGENT = 173, UPDATE_AGENT = 173,
SEND_UPDATE_COMMAND = 174, SEND_UPDATE_COMMAND = 174,
@ -94,9 +97,13 @@ export enum PermissionEnum {
ADD_APP_TO_SELECTED = 185, ADD_APP_TO_SELECTED = 185,
DEL_APP_FROM_SELECTED = 186, DEL_APP_FROM_SELECTED = 186,
// AUDIT
AUDIT_OPERATION = 190,
VIEW_AUDIT_LOGS = 191,
//Undefined //Undefined
UNDEFINED = 9999, UNDEFINED = 9999,
//Allow All //Allow All
ALLOW_ALL = 0, ALLOW_ALL = 0
} }