Add role and permission config

This commit is contained in:
Do Manh Phuong 2026-03-04 14:41:34 +07:00
parent beb79025b2
commit 5e29ac78f7
41 changed files with 3323 additions and 1020 deletions

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/TTMT.ManageWebGUI.iml" filepath="$PROJECT_DIR$/.idea/TTMT.ManageWebGUI.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

74
.idea/workspace.xml Normal file
View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="ChangeListManager">
<list default="true" id="94f75d5b-3c15-4d7e-9da7-ad6f5328faf8" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/src/hooks/useAuth.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/src/hooks/useAuth.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/routeTree.gen.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/routeTree.gen.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/routes/_auth/blacklist/index.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/routes/_auth/command/index.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/routes/_auth/room/$roomName/index.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/routes/_auth/room/index.tsx" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/src/types/app-sidebar.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/app-sidebar.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/types/permission.ts" beforeDir="false" afterPath="$PROJECT_DIR$/src/types/permission.ts" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 1
}]]></component>
<component name="ProjectId" id="3AQVfIkiaizPRlnpMICDG3COfJV" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
"dart.analysis.tool.window.visible": "false",
"git-widget-placeholder": "main",
"ignore.virus.scanning.warn.message": "true",
"javascript.preferred.runtime.type.id": "node",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"ts.external.directory.path": "D:\\MyProject\\NAVISProject\\TTMT.ManageWebGUI\\node_modules\\typescript\\lib",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-d6986cc7102b-9b0f141eb926-JavaScript-WS-253.31033.133" />
</set>
</attachedChunks>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="94f75d5b-3c15-4d7e-9da7-ad6f5328faf8" name="Changes" comment="" />
<created>1772524885874</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1772524885874</updated>
<workItem from="1772524887267" duration="1839000" />
</task>
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
</project>

26
debug-permissions.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<title>Debug Permissions</title>
<script>
// Paste this in browser console to debug
const acs = localStorage.getItem('acs');
const role = localStorage.getItem('role');
console.log('Current Role:', role);
console.log('Current ACS (raw):', acs);
console.log('Current ACS (parsed):', acs ? acs.split(',').map(Number) : []);
console.log('VIEW_AGENT permission code:', 171);
console.log('Has VIEW_AGENT?', acs ? acs.split(',').map(Number).includes(171) : false);
</script>
</head>
<body>
<h1>Open Browser Console (F12)</h1>
<p>Or run this in console:</p>
<pre>
const acs = localStorage.getItem('acs');
console.log('Your permissions:', acs);
console.log('As array:', acs ? acs.split(',').map(Number) : []);
console.log('Has VIEW_AGENT (171)?', acs ? acs.split(',').includes('171') : false);
</pre>
</body>
</html>

1754
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -34,8 +34,10 @@
"axios": "^1.11.0", "axios": "^1.11.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0",
"lucide-react": "^0.525.0", "lucide-react": "^0.525.0",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"radix-ui": "^1.4.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"shadcn": "^2.9.3", "shadcn": "^2.9.3",

View File

@ -0,0 +1,37 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator
} from "./ui/breadcrumb";
import { Link, useMatches } from "@tanstack/react-router";
export default function AppBreadCrumb() {
const matches = useMatches();
const crumbs = matches
.filter((m) => Boolean(m.context.breadcrumbs))
.map((m) => m.context.breadcrumbs)
.filter(Boolean);
const displayCrumbs = crumbs[0] as { path: string; title: string }[];
if (displayCrumbs == null || displayCrumbs.length == 0) return;
return (
<Breadcrumb className="flex-1">
<BreadcrumbList>
{displayCrumbs.slice(0, -1).map((b, index) => (
<>
<BreadcrumbItem key={index} className="md:block">
<Link to={b.path}>{b.title}</Link>
</BreadcrumbItem>
<BreadcrumbSeparator className="hidden md:block" />
</>
))}
<BreadcrumbItem className="md:block">
<BreadcrumbPage>{displayCrumbs[displayCrumbs.length - 1].title}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
);
}

View File

@ -0,0 +1,85 @@
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { LogOut, Settings, User } from "lucide-react";
interface AvatarDropdownProps {
username: string;
role: {
roleName: string;
priority: number;
};
onLogOut: () => void;
onSettings?: () => void;
onProfile?: () => void;
}
export default function AvatarDropdown({
username,
role,
onLogOut,
onSettings,
onProfile,
}: AvatarDropdownProps) {
// Get initials from username
const getInitials = (name: string): string => {
if (!name) return "U";
const parts = name.split(" ");
if (parts.length >= 2) {
return `${parts[0][0]}${parts[1][0]}`.toUpperCase();
}
return name.substring(0, 2).toUpperCase();
};
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 rounded-full focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2">
<Avatar className="h-9 w-9 cursor-pointer">
<AvatarImage src="" alt={username} />
<AvatarFallback className="bg-primary text-primary-foreground text-sm font-medium">
{getInitials(username)}
</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuLabel className="font-normal">
<div className="flex flex-col space-y-1">
<p className="text-sm font-medium leading-none">{username}</p>
<p className="text-xs leading-none text-muted-foreground">
{role.roleName || "Người dùng"}
</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{onProfile && (
<DropdownMenuItem onClick={onProfile} className="cursor-pointer">
<User className="mr-2 h-4 w-4" />
<span>Tài khoản của tôi</span>
</DropdownMenuItem>
)}
{onSettings && (
<DropdownMenuItem onClick={onSettings} className="cursor-pointer">
<Settings className="mr-2 h-4 w-4" />
<span>Cài đt</span>
</DropdownMenuItem>
)}
{(onProfile || onSettings) && <DropdownMenuSeparator />}
<DropdownMenuItem
onClick={onLogOut}
className="cursor-pointer text-destructive focus:text-destructive"
>
<LogOut className="mr-2 h-4 w-4" />
<span>Đăng xuất</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -0,0 +1,16 @@
import { AxiosError } from "axios";
export function ErrorFetchingPage({ error }: { error: Error | AxiosError }) {
return (
<div className="p-4 text-destructive">
<h2 className="text-xl font-semibold mb-2">Lỗi</h2>
<p>
{"isAxiosError" in error &&
error.response?.data &&
(error.response.data as { message?: string }).message
? (error.response.data as { message?: string }).message
: "Lỗi trong quá trình render page"}
</p>
</div>
);
}

View File

@ -0,0 +1,43 @@
import { Button } from "@/components/ui/button";
import { Route } from "@/routes/_auth";
import { useRouter } from "@tanstack/react-router";
import { ArrowLeft, Search } from "lucide-react";
import { useAuth } from "@/hooks/useAuth";
export default function SessionTimeOutErrorPage() {
const router = useRouter();
const navigate = Route.useNavigate();
const auth = useAuth();
const handleLogout = () => {
auth.logout();
router.invalidate().finally(() => {
navigate({ to: "/login" });
});
};
return (
<div className="flex flex-col items-center justify-center min-h-[70vh] px-4 text-center">
<div className="space-y-6 max-w-md mx-auto">
<div className="relative mx-auto w-40 h-40 md:w-52 md:h-52">
<div className="absolute inset-0 bg-primary/10 rounded-full animate-pulse" />
<div className="absolute inset-0 flex items-center justify-center">
<Search className="h-20 w-20 md:h-24 md:w-24 text-primary" strokeWidth={1.5} />
</div>
</div>
<div className="space-y-3">
<h2 className="text-2xl md:text-3xl font-semibold">Phiên hết hạn</h2>
<p className="text-muted-foreground">Bạn cần đăng nhập lại</p>
</div>
<div className="pt-6">
<Button asChild size="lg" className="gap-2" onClick={handleLogout}>
<div className="flex items-center gap-1">
<ArrowLeft className="h-4 w-4" />
Trở về trang đăng nhập
</div>
</Button>
</div>
</div>
</div>
);
}

View File

@ -14,19 +14,53 @@ import {
SidebarMenuItem, SidebarMenuItem,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { appSidebarSection } from "@/types/app-sidebar";
import { PermissionEnum } from "@/types/permission";
import { useAuth } from "@/hooks/useAuth";
import { useMemo } from "react";
type MenuItem = { type SidebarItem = {
title: string; title: string;
to: string; url: string;
code?: number;
icon: React.ElementType; icon: React.ElementType;
onPointerEnter?: () => void; permissions?: PermissionEnum[];
}; };
type AppSidebarProps = { type SidebarSection = {
items: MenuItem[]; title: string;
items: SidebarItem[];
}; };
export function AppSidebar({ items }: AppSidebarProps) { export function AppSidebar() {
const { hasPermission, acs } = useAuth();
// Check if user is admin (has ALLOW_ALL permission)
const isAdmin = acs.includes(PermissionEnum.ALLOW_ALL);
// Check if user has any of the required permissions
const checkPermissions = (permissions?: PermissionEnum[]) => {
// No permissions defined = show to everyone
if (!permissions || permissions.length === 0) return true;
// Item marked as ALLOW_ALL = show to everyone
if (permissions.includes(PermissionEnum.ALLOW_ALL)) return true;
// Admin users see everything
if (isAdmin) return true;
// Check if user has any of the required permissions
return permissions.some((permission) => hasPermission(permission));
};
// Filter sidebar sections and items based on permissions
const filteredNavMain = useMemo(() => {
return appSidebarSection.navMain
.map((section) => ({
...section,
items: section.items.filter((item) => checkPermissions(item.permissions)),
}))
.filter((section) => section.items.length > 0) as SidebarSection[];
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [acs]);
return ( return (
<Sidebar <Sidebar
collapsible="icon" collapsible="icon"
@ -50,18 +84,18 @@ export function AppSidebar({ items }: AppSidebarProps) {
</SidebarHeader> </SidebarHeader>
<SidebarContent className="p-4"> <SidebarContent className="p-4">
<SidebarGroup> {filteredNavMain.map((section) => (
<SidebarGroup key={section.title}>
<SidebarGroupLabel className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider mb-2"> <SidebarGroupLabel className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider mb-2">
Navigation {section.title}
</SidebarGroupLabel> </SidebarGroupLabel>
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu className="space-y-1"> <SidebarMenu className="space-y-1">
{items.map((item) => ( {section.items.map((item) => (
<SidebarMenuItem key={item.title}> <SidebarMenuItem key={item.title}>
<SidebarMenuButton <SidebarMenuButton
asChild asChild
tooltip={item.title} tooltip={item.title}
onPointerEnter={item.onPointerEnter}
className={cn( className={cn(
"w-full justify-start gap-3 px-4 py-3 rounded-xl", "w-full justify-start gap-3 px-4 py-3 rounded-xl",
"hover:bg-accent/60 hover:text-accent-foreground hover:shadow-sm", "hover:bg-accent/60 hover:text-accent-foreground hover:shadow-sm",
@ -72,8 +106,8 @@ export function AppSidebar({ items }: AppSidebarProps) {
)} )}
> >
<Link <Link
href={item.to} href={item.url}
to={"."} to={item.url}
className="flex items-center gap-3 w-full" className="flex items-center gap-3 w-full"
> >
<item.icon className="size-5 shrink-0 transition-all duration-200 group-hover:scale-110 group-hover:text-primary" /> <item.icon className="size-5 shrink-0 transition-all duration-200 group-hover:scale-110 group-hover:text-primary" />
@ -87,6 +121,7 @@ export function AppSidebar({ items }: AppSidebarProps) {
</SidebarMenu> </SidebarMenu>
</SidebarGroupContent> </SidebarGroupContent>
</SidebarGroup> </SidebarGroup>
))}
</SidebarContent> </SidebarContent>
<SidebarFooter className="border-t border-border/40 p-4 space-y-3"> <SidebarFooter className="border-t border-border/40 p-4 space-y-3">

View File

@ -0,0 +1,109 @@
import * as React from "react"
import { ChevronRight, MoreHorizontal } from "lucide-react"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />
}
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="breadcrumb-list"
className={cn(
"flex flex-wrap items-center gap-1.5 text-sm break-words text-muted-foreground sm:gap-2.5",
className
)}
{...props}
/>
)
}
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-item"
className={cn("inline-flex items-center gap-1.5", className)}
{...props}
/>
)
}
function BreadcrumbLink({
asChild,
className,
...props
}: React.ComponentProps<"a"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "a"
return (
<Comp
data-slot="breadcrumb-link"
className={cn("transition-colors hover:text-foreground", className)}
{...props}
/>
)
}
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-page"
role="link"
aria-disabled="true"
aria-current="page"
className={cn("font-normal text-foreground", className)}
{...props}
/>
)
}
function BreadcrumbSeparator({
children,
className,
...props
}: React.ComponentProps<"li">) {
return (
<li
data-slot="breadcrumb-separator"
role="presentation"
aria-hidden="true"
className={cn("[&>svg]:size-3.5", className)}
{...props}
>
{children ?? <ChevronRight />}
</li>
)
}
function BreadcrumbEllipsis({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="breadcrumb-ellipsis"
role="presentation"
aria-hidden="true"
className={cn("flex size-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="size-4" />
<span className="sr-only">More</span>
</span>
)
}
export {
Breadcrumb,
BreadcrumbList,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbPage,
BreadcrumbSeparator,
BreadcrumbEllipsis,
}

View File

@ -47,8 +47,7 @@ export const API_ENDPOINTS = {
REQUEST_GET_CLIENT_FOLDER_STATUS: (roomName: string) => REQUEST_GET_CLIENT_FOLDER_STATUS: (roomName: string) =>
`${BASE_URL}/DeviceComm/clientfolderstatus/${roomName}`, `${BASE_URL}/DeviceComm/clientfolderstatus/${roomName}`,
}, },
COMMAND: COMMAND: {
{
ADD_COMMAND: `${BASE_URL}/Command/add`, ADD_COMMAND: `${BASE_URL}/Command/add`,
GET_COMMANDS: `${BASE_URL}/Command/all`, GET_COMMANDS: `${BASE_URL}/Command/all`,
GET_COMMAND_BY_TYPES: (types: string) => `${BASE_URL}/Command/types/${types}`, GET_COMMAND_BY_TYPES: (types: string) => `${BASE_URL}/Command/types/${types}`,
@ -61,4 +60,49 @@ export const API_ENDPOINTS = {
GET_PROCESSES_LISTS: `${BASE_URL}/Sse/events/processLists`, GET_PROCESSES_LISTS: `${BASE_URL}/Sse/events/processLists`,
GET_CLIENT_FOLDER_STATUS: `${BASE_URL}/Sse/events/clientFolderStatuses`, GET_CLIENT_FOLDER_STATUS: `${BASE_URL}/Sse/events/clientFolderStatuses`,
}, },
PERMISSION: {
// Lấy danh sách permission từ enum
GET_LIST: `${BASE_URL}/Permission/list`,
// Lấy permission theo category
GET_BY_CATEGORY: `${BASE_URL}/Permission/list-by-category`,
// Lấy chi tiết permission theo value
GET_BY_VALUE: (value: number) => `${BASE_URL}/Permission/${value}`,
// Import permission từ enum vào DB (chạy 1 lần đầu tiên)
SEED_FROM_ENUM: `${BASE_URL}/Permission/seed-from-enum`,
// Lấy danh sách permission từ database
GET_DB_LIST: `${BASE_URL}/Permission/db-list`,
// Xóa permission
DELETE: (id: number) => `${BASE_URL}/Permission/${id}`,
},
ROLE: {
// Lấy danh sách tất cả roles
GET_LIST: `${BASE_URL}/Role/list`,
// Lấy chi tiết role theo ID
GET_BY_ID: (id: number) => `${BASE_URL}/Role/${id}`,
// Tạo role mới
CREATE: `${BASE_URL}/Role/create`,
// Cập nhật role
UPDATE: (id: number) => `${BASE_URL}/Role/update/${id}`,
// Xóa role
DELETE: (id: number) => `${BASE_URL}/Role/${id}`,
// Lấy danh sách permissions của role
GET_PERMISSIONS: (id: number) => `${BASE_URL}/Role/${id}/permissions`,
// Gán permissions cho role (thay thế toàn bộ)
ASSIGN_PERMISSIONS: (id: number) => `${BASE_URL}/Role/${id}/assign-permissions`,
// Bật/tắt một permission cụ thể (query param: ?isChecked=true/false)
TOGGLE_PERMISSION: (roleId: number, permissionId: number) =>
`${BASE_URL}/Role/${roleId}/permissions/${permissionId}/toggle`,
},
}; };

View File

@ -9,3 +9,9 @@ export * from "./useDeviceCommQueries";
// Command Queries // Command Queries
export * from "./useCommandQueries"; export * from "./useCommandQueries";
// Permission Queries
export * from "./usePermissionQueries";
// Role Queries
export * from "./useRoleQueries";

View File

@ -0,0 +1,90 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as permissionService from "@/services/permission.service";
export const PERMISSION_QUERY_KEYS = {
all: ["permissions"] as const,
list: () => [...PERMISSION_QUERY_KEYS.all, "list"] as const,
dbList: () => [...PERMISSION_QUERY_KEYS.all, "db-list"] as const,
byCategory: () => [...PERMISSION_QUERY_KEYS.all, "by-category"] as const,
detail: (value: number) => [...PERMISSION_QUERY_KEYS.all, "detail", value] as const,
};
/**
* Hook đ lấy danh sách permission từ enum
*/
export function useGetPermissionList(enabled = true) {
return useQuery({
queryKey: PERMISSION_QUERY_KEYS.list(),
queryFn: () => permissionService.getPermissionList(),
enabled,
staleTime: 10 * 60 * 1000, // 10 minutes
});
}
/**
* Hook đ lấy permission theo category
*/
export function useGetPermissionByCategory(enabled = true) {
return useQuery({
queryKey: PERMISSION_QUERY_KEYS.byCategory(),
queryFn: () => permissionService.getPermissionByCategory(),
enabled,
staleTime: 10 * 60 * 1000, // 10 minutes
});
}
/**
* Hook đ lấy chi tiết permission theo value
*/
export function useGetPermissionByValue(value: number, enabled = true) {
return useQuery({
queryKey: PERMISSION_QUERY_KEYS.detail(value),
queryFn: () => permissionService.getPermissionByValue(value),
enabled: enabled && value > 0,
staleTime: 10 * 60 * 1000, // 10 minutes
});
}
/**
* Hook đ lấy danh sách permission từ database
*/
export function useGetPermissionDbList(enabled = true) {
return useQuery({
queryKey: PERMISSION_QUERY_KEYS.dbList(),
queryFn: () => permissionService.getPermissionDbList(),
enabled,
staleTime: 10 * 60 * 1000, // 10 minutes
});
}
/**
* Hook đ seed permission từ enum vào DB
*/
export function useSeedPermissionFromEnum() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => permissionService.seedPermissionFromEnum(),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: PERMISSION_QUERY_KEYS.all,
});
},
});
}
/**
* Hook đ xóa permission
*/
export function useDeletePermission() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => permissionService.deletePermission(id),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: PERMISSION_QUERY_KEYS.all,
});
},
});
}

View File

@ -0,0 +1,149 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import * as roleService from "@/services/role.service";
import type { TCreateRoleRequestBody } from "@/types/role";
export const ROLE_QUERY_KEYS = {
all: ["roles"] as const,
list: () => [...ROLE_QUERY_KEYS.all, "list"] as const,
detail: (id: number) => [...ROLE_QUERY_KEYS.all, "detail", id] as const,
permissions: (id: number) => [...ROLE_QUERY_KEYS.all, "permissions", id] as const,
};
/**
* Hook đ lấy danh sách roles
*/
export function useGetRoleList(enabled = true) {
return useQuery({
queryKey: ROLE_QUERY_KEYS.list(),
queryFn: () => roleService.getRoleList(),
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
/**
* Hook đ lấy chi tiết role theo ID
*/
export function useGetRoleById(id: number, enabled = true) {
return useQuery({
queryKey: ROLE_QUERY_KEYS.detail(id),
queryFn: () => roleService.getRoleById(id),
enabled: enabled && id > 0,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
/**
* Hook đ lấy danh sách permissions của role
*/
export function useGetRolePermissions(id: number, enabled = true) {
return useQuery({
queryKey: ROLE_QUERY_KEYS.permissions(id),
queryFn: () => roleService.getRolePermissions(id),
enabled: enabled && id > 0,
staleTime: 5 * 60 * 1000, // 5 minutes
});
}
/**
* Hook đ tạo role mới
*/
export function useCreateRole() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: TCreateRoleRequestBody) => roleService.createRole(data),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ROLE_QUERY_KEYS.list(),
});
},
});
}
/**
* Hook đ cập nhật role
*/
export function useUpdateRole() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
id,
data,
}: {
id: number;
data: Partial<TCreateRoleRequestBody>;
}) => roleService.updateRole(id, data),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ROLE_QUERY_KEYS.list(),
});
queryClient.invalidateQueries({
queryKey: ROLE_QUERY_KEYS.detail(variables.id),
});
},
});
}
/**
* Hook đ xóa role
*/
export function useDeleteRole() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => roleService.deleteRole(id),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ROLE_QUERY_KEYS.list(),
});
},
});
}
/**
* Hook đ gán permissions cho role
*/
export function useAssignRolePermissions() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
roleId,
permissionIds,
}: {
roleId: number;
permissionIds: number[];
}) => roleService.assignRolePermissions(roleId, permissionIds),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ROLE_QUERY_KEYS.permissions(variables.roleId),
});
},
});
}
/**
* Hook đ bật/tắt một permission của role
*/
export function useToggleRolePermission() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({
roleId,
permissionId,
isChecked,
}: {
roleId: number;
permissionId: number;
isChecked: boolean;
}) => roleService.toggleRolePermission(roleId, permissionId, isChecked),
onSuccess: (_, variables) => {
queryClient.invalidateQueries({
queryKey: ROLE_QUERY_KEYS.permissions(variables.roleId),
});
},
});
}

View File

@ -21,7 +21,7 @@ export interface IAuthContext {
const AuthContext = React.createContext<IAuthContext | null>(null); const AuthContext = React.createContext<IAuthContext | null>(null);
const key = "accesscontrol.auth.user"; const key = "computersmanagement.auth.user";
function getStoredUser() { function getStoredUser() {
return localStorage.getItem(key); return localStorage.getItem(key);

View File

@ -5,89 +5,19 @@ import {
SidebarInset, SidebarInset,
SidebarTrigger, SidebarTrigger,
} from "@/components/ui/sidebar"; } from "@/components/ui/sidebar";
import { Home, Building, AppWindow, Terminal, CircleX } from "lucide-react"; import { Building } from "lucide-react";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { useQueryClient } from "@tanstack/react-query";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import {
useGetAgentVersion,
useGetSoftwareList,
useGetRoomList,
useGetBlacklist,
} from "@/hooks/queries";
type AppLayoutProps = { type AppLayoutProps = {
children: ReactNode; children: ReactNode;
}; };
export default function AppLayout({ children }: AppLayoutProps) { export default function AppLayout({ children }: AppLayoutProps) {
const queryClient = useQueryClient();
const handlePrefetchAgents = () => {
queryClient.prefetchQuery({
queryKey: ["app-version", "agent"],
queryFn: useGetAgentVersion as any,
staleTime: 60 * 1000,
});
};
const handlePrefetchSofware = () => {
queryClient.prefetchQuery({
queryKey: ["app-version", "software"],
queryFn: useGetSoftwareList as any,
staleTime: 60 * 1000,
});
};
const handlePrefetchRooms = () => {
queryClient.prefetchQuery({
queryKey: ["device-comm", "rooms"],
queryFn: useGetRoomList as any,
staleTime: 5 * 60 * 1000,
});
};
const handlePrefetchBannedSoftware = () => {
queryClient.prefetchQuery({
queryKey: ["app-version", "blacklist"],
queryFn: useGetBlacklist as any,
staleTime: 60 * 1000,
});
};
const items = [
{ title: "Dashboard", to: "/", icon: Home },
{
title: "Danh sách phòng",
to: "/room",
icon: Building,
onPointerEnter: handlePrefetchRooms,
},
{
title: "Quản lý Agent",
to: "/agent",
icon: AppWindow,
onPointerEnter: handlePrefetchAgents,
},
{
title: "Quản lý phần mềm",
to: "/apps",
icon: AppWindow,
onPointerEnter: handlePrefetchSofware,
},
{ title: "Gửi lệnh CMD", to: "/command", icon: Terminal },
{
title: "Danh sách đen",
to: "/blacklist",
icon: CircleX,
onPointerEnter: handlePrefetchBannedSoftware,
},
];
return ( return (
<SidebarProvider> <SidebarProvider>
<div className="flex min-h-screen w-full bg-background"> <div className="flex min-h-screen w-full bg-background">
<AppSidebar items={items} /> <AppSidebar />
<SidebarInset className="flex-1"> <SidebarInset className="flex-1">
<header className="flex h-16 shrink-0 items-center gap-2 border-b border-border/40 bg-background/95 backdrop-blur px-4 lg:hidden supports-[backdrop-filter]:bg-background/60"> <header className="flex h-16 shrink-0 items-center gap-2 border-b border-border/40 bg-background/95 backdrop-blur px-4 lg:hidden supports-[backdrop-filter]:bg-background/60">
<SidebarTrigger className="-ml-1 hover:bg-accent/50 rounded-lg p-2 transition-colors" /> <SidebarTrigger className="-ml-1 hover:bg-accent/50 rounded-lg p-2 transition-colors" />

View File

@ -1,4 +1,5 @@
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx"
import { format } from "date-fns";
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
@ -8,3 +9,27 @@ export function cn(...inputs: ClassValue[]) {
export function sleep(ms: number): Promise<void> { export function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)) return new Promise((resolve) => setTimeout(resolve, ms))
} }
const DEFAULT_DATE_FORMAT = "dd/MM/yyyy";
export function getCurrentTimeUTC(): string {
const date = new Date();
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0");
const day = date.getUTCDate().toString().padStart(2, "0");
const year = date.getUTCFullYear();
const hour = date.getUTCHours().toString().padStart(2, "0");
const min = date.getUTCMinutes().toString().padStart(2, "0");
return `${day}/${month}/${year} ${hour}:${min}`;
}
export function formatDate(
date: string | Date | undefined | null,
formatTemplate = DEFAULT_DATE_FORMAT
) {
if (date == undefined) return "";
if (date == null) return "";
return format(new Date(date), formatTemplate, {});
}

View File

@ -11,15 +11,18 @@
import { Route as rootRouteImport } from './routes/__root' import { Route as rootRouteImport } from './routes/__root'
import { Route as AuthRouteImport } from './routes/_auth' import { Route as AuthRouteImport } from './routes/_auth'
import { Route as IndexRouteImport } from './routes/index' import { Route as IndexRouteImport } from './routes/index'
import { Route as AuthRoomIndexRouteImport } from './routes/_auth/room/index' import { Route as AuthRoomsIndexRouteImport } from './routes/_auth/rooms/index'
import { Route as AuthRoleIndexRouteImport } from './routes/_auth/role/index'
import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index' 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 AuthCommandIndexRouteImport } from './routes/_auth/command/index' import { Route as AuthCommandsIndexRouteImport } from './routes/_auth/commands/index'
import { Route as AuthBlacklistIndexRouteImport } from './routes/_auth/blacklist/index' import { Route as AuthBlacklistsIndexRouteImport } from './routes/_auth/blacklists/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'
import { Route as AuthRoomRoomNameIndexRouteImport } from './routes/_auth/room/$roomName/index' import { Route as AuthRoomsRoomNameIndexRouteImport } from './routes/_auth/rooms/$roomName/index'
import { Route as AuthRoleCreateIndexRouteImport } from './routes/_auth/role/create/index'
import { Route as AuthRoleIdEditIndexRouteImport } from './routes/_auth/role/$id/edit/index'
const AuthRoute = AuthRouteImport.update({ const AuthRoute = AuthRouteImport.update({
id: '/_auth', id: '/_auth',
@ -30,9 +33,14 @@ const IndexRoute = IndexRouteImport.update({
path: '/', path: '/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AuthRoomIndexRoute = AuthRoomIndexRouteImport.update({ const AuthRoomsIndexRoute = AuthRoomsIndexRouteImport.update({
id: '/room/', id: '/rooms/',
path: '/room/', path: '/rooms/',
getParentRoute: () => AuthRoute,
} as any)
const AuthRoleIndexRoute = AuthRoleIndexRouteImport.update({
id: '/role/',
path: '/role/',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
const AuthDeviceIndexRoute = AuthDeviceIndexRouteImport.update({ const AuthDeviceIndexRoute = AuthDeviceIndexRouteImport.update({
@ -45,14 +53,14 @@ const AuthDashboardIndexRoute = AuthDashboardIndexRouteImport.update({
path: '/dashboard/', path: '/dashboard/',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
const AuthCommandIndexRoute = AuthCommandIndexRouteImport.update({ const AuthCommandsIndexRoute = AuthCommandsIndexRouteImport.update({
id: '/command/', id: '/commands/',
path: '/command/', path: '/commands/',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
const AuthBlacklistIndexRoute = AuthBlacklistIndexRouteImport.update({ const AuthBlacklistsIndexRoute = AuthBlacklistsIndexRouteImport.update({
id: '/blacklist/', id: '/blacklists/',
path: '/blacklist/', path: '/blacklists/',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({ const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({
@ -70,9 +78,19 @@ const authLoginIndexRoute = authLoginIndexRouteImport.update({
path: '/login/', path: '/login/',
getParentRoute: () => rootRouteImport, getParentRoute: () => rootRouteImport,
} as any) } as any)
const AuthRoomRoomNameIndexRoute = AuthRoomRoomNameIndexRouteImport.update({ const AuthRoomsRoomNameIndexRoute = AuthRoomsRoomNameIndexRouteImport.update({
id: '/room/$roomName/', id: '/rooms/$roomName/',
path: '/room/$roomName/', path: '/rooms/$roomName/',
getParentRoute: () => AuthRoute,
} as any)
const AuthRoleCreateIndexRoute = AuthRoleCreateIndexRouteImport.update({
id: '/role/create/',
path: '/role/create/',
getParentRoute: () => AuthRoute,
} as any)
const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({
id: '/role/$id/edit/',
path: '/role/$id/edit/',
getParentRoute: () => AuthRoute, getParentRoute: () => AuthRoute,
} as any) } as any)
@ -81,24 +99,30 @@ export interface FileRoutesByFullPath {
'/login': typeof authLoginIndexRoute '/login': typeof authLoginIndexRoute
'/agent': typeof AuthAgentIndexRoute '/agent': typeof AuthAgentIndexRoute
'/apps': typeof AuthAppsIndexRoute '/apps': typeof AuthAppsIndexRoute
'/blacklist': typeof AuthBlacklistIndexRoute '/blacklists': typeof AuthBlacklistsIndexRoute
'/command': typeof AuthCommandIndexRoute '/commands': typeof AuthCommandsIndexRoute
'/dashboard': typeof AuthDashboardIndexRoute '/dashboard': typeof AuthDashboardIndexRoute
'/device': typeof AuthDeviceIndexRoute '/device': typeof AuthDeviceIndexRoute
'/room': typeof AuthRoomIndexRoute '/role': typeof AuthRoleIndexRoute
'/room/$roomName': typeof AuthRoomRoomNameIndexRoute '/rooms': typeof AuthRoomsIndexRoute
'/role/create': typeof AuthRoleCreateIndexRoute
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
} }
export interface FileRoutesByTo { export interface FileRoutesByTo {
'/': typeof IndexRoute '/': typeof IndexRoute
'/login': typeof authLoginIndexRoute '/login': typeof authLoginIndexRoute
'/agent': typeof AuthAgentIndexRoute '/agent': typeof AuthAgentIndexRoute
'/apps': typeof AuthAppsIndexRoute '/apps': typeof AuthAppsIndexRoute
'/blacklist': typeof AuthBlacklistIndexRoute '/blacklists': typeof AuthBlacklistsIndexRoute
'/command': typeof AuthCommandIndexRoute '/commands': typeof AuthCommandsIndexRoute
'/dashboard': typeof AuthDashboardIndexRoute '/dashboard': typeof AuthDashboardIndexRoute
'/device': typeof AuthDeviceIndexRoute '/device': typeof AuthDeviceIndexRoute
'/room': typeof AuthRoomIndexRoute '/role': typeof AuthRoleIndexRoute
'/room/$roomName': typeof AuthRoomRoomNameIndexRoute '/rooms': typeof AuthRoomsIndexRoute
'/role/create': typeof AuthRoleCreateIndexRoute
'/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute
'/role/$id/edit': typeof AuthRoleIdEditIndexRoute
} }
export interface FileRoutesById { export interface FileRoutesById {
__root__: typeof rootRouteImport __root__: typeof rootRouteImport
@ -107,12 +131,15 @@ 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/blacklist/': typeof AuthBlacklistIndexRoute '/_auth/blacklists/': typeof AuthBlacklistsIndexRoute
'/_auth/command/': typeof AuthCommandIndexRoute '/_auth/commands/': typeof AuthCommandsIndexRoute
'/_auth/dashboard/': typeof AuthDashboardIndexRoute '/_auth/dashboard/': typeof AuthDashboardIndexRoute
'/_auth/device/': typeof AuthDeviceIndexRoute '/_auth/device/': typeof AuthDeviceIndexRoute
'/_auth/room/': typeof AuthRoomIndexRoute '/_auth/role/': typeof AuthRoleIndexRoute
'/_auth/room/$roomName/': typeof AuthRoomRoomNameIndexRoute '/_auth/rooms/': typeof AuthRoomsIndexRoute
'/_auth/role/create/': typeof AuthRoleCreateIndexRoute
'/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute
'/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute
} }
export interface FileRouteTypes { export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath fileRoutesByFullPath: FileRoutesByFullPath
@ -121,24 +148,30 @@ export interface FileRouteTypes {
| '/login' | '/login'
| '/agent' | '/agent'
| '/apps' | '/apps'
| '/blacklist' | '/blacklists'
| '/command' | '/commands'
| '/dashboard' | '/dashboard'
| '/device' | '/device'
| '/room' | '/role'
| '/room/$roomName' | '/rooms'
| '/role/create'
| '/rooms/$roomName'
| '/role/$id/edit'
fileRoutesByTo: FileRoutesByTo fileRoutesByTo: FileRoutesByTo
to: to:
| '/' | '/'
| '/login' | '/login'
| '/agent' | '/agent'
| '/apps' | '/apps'
| '/blacklist' | '/blacklists'
| '/command' | '/commands'
| '/dashboard' | '/dashboard'
| '/device' | '/device'
| '/room' | '/role'
| '/room/$roomName' | '/rooms'
| '/role/create'
| '/rooms/$roomName'
| '/role/$id/edit'
id: id:
| '__root__' | '__root__'
| '/' | '/'
@ -146,12 +179,15 @@ export interface FileRouteTypes {
| '/(auth)/login/' | '/(auth)/login/'
| '/_auth/agent/' | '/_auth/agent/'
| '/_auth/apps/' | '/_auth/apps/'
| '/_auth/blacklist/' | '/_auth/blacklists/'
| '/_auth/command/' | '/_auth/commands/'
| '/_auth/dashboard/' | '/_auth/dashboard/'
| '/_auth/device/' | '/_auth/device/'
| '/_auth/room/' | '/_auth/role/'
| '/_auth/room/$roomName/' | '/_auth/rooms/'
| '/_auth/role/create/'
| '/_auth/rooms/$roomName/'
| '/_auth/role/$id/edit/'
fileRoutesById: FileRoutesById fileRoutesById: FileRoutesById
} }
export interface RootRouteChildren { export interface RootRouteChildren {
@ -176,11 +212,18 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/_auth/room/': { '/_auth/rooms/': {
id: '/_auth/room/' id: '/_auth/rooms/'
path: '/room' path: '/rooms'
fullPath: '/room' fullPath: '/rooms'
preLoaderRoute: typeof AuthRoomIndexRouteImport preLoaderRoute: typeof AuthRoomsIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/role/': {
id: '/_auth/role/'
path: '/role'
fullPath: '/role'
preLoaderRoute: typeof AuthRoleIndexRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
'/_auth/device/': { '/_auth/device/': {
@ -197,18 +240,18 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthDashboardIndexRouteImport preLoaderRoute: typeof AuthDashboardIndexRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
'/_auth/command/': { '/_auth/commands/': {
id: '/_auth/command/' id: '/_auth/commands/'
path: '/command' path: '/commands'
fullPath: '/command' fullPath: '/commands'
preLoaderRoute: typeof AuthCommandIndexRouteImport preLoaderRoute: typeof AuthCommandsIndexRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
'/_auth/blacklist/': { '/_auth/blacklists/': {
id: '/_auth/blacklist/' id: '/_auth/blacklists/'
path: '/blacklist' path: '/blacklists'
fullPath: '/blacklist' fullPath: '/blacklists'
preLoaderRoute: typeof AuthBlacklistIndexRouteImport preLoaderRoute: typeof AuthBlacklistsIndexRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
'/_auth/apps/': { '/_auth/apps/': {
@ -232,11 +275,25 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof authLoginIndexRouteImport preLoaderRoute: typeof authLoginIndexRouteImport
parentRoute: typeof rootRouteImport parentRoute: typeof rootRouteImport
} }
'/_auth/room/$roomName/': { '/_auth/rooms/$roomName/': {
id: '/_auth/room/$roomName/' id: '/_auth/rooms/$roomName/'
path: '/room/$roomName' path: '/rooms/$roomName'
fullPath: '/room/$roomName' fullPath: '/rooms/$roomName'
preLoaderRoute: typeof AuthRoomRoomNameIndexRouteImport preLoaderRoute: typeof AuthRoomsRoomNameIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/role/create/': {
id: '/_auth/role/create/'
path: '/role/create'
fullPath: '/role/create'
preLoaderRoute: typeof AuthRoleCreateIndexRouteImport
parentRoute: typeof AuthRoute
}
'/_auth/role/$id/edit/': {
id: '/_auth/role/$id/edit/'
path: '/role/$id/edit'
fullPath: '/role/$id/edit'
preLoaderRoute: typeof AuthRoleIdEditIndexRouteImport
parentRoute: typeof AuthRoute parentRoute: typeof AuthRoute
} }
} }
@ -245,23 +302,29 @@ declare module '@tanstack/react-router' {
interface AuthRouteChildren { interface AuthRouteChildren {
AuthAgentIndexRoute: typeof AuthAgentIndexRoute AuthAgentIndexRoute: typeof AuthAgentIndexRoute
AuthAppsIndexRoute: typeof AuthAppsIndexRoute AuthAppsIndexRoute: typeof AuthAppsIndexRoute
AuthBlacklistIndexRoute: typeof AuthBlacklistIndexRoute AuthBlacklistsIndexRoute: typeof AuthBlacklistsIndexRoute
AuthCommandIndexRoute: typeof AuthCommandIndexRoute AuthCommandsIndexRoute: typeof AuthCommandsIndexRoute
AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute
AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute
AuthRoomIndexRoute: typeof AuthRoomIndexRoute AuthRoleIndexRoute: typeof AuthRoleIndexRoute
AuthRoomRoomNameIndexRoute: typeof AuthRoomRoomNameIndexRoute AuthRoomsIndexRoute: typeof AuthRoomsIndexRoute
AuthRoleCreateIndexRoute: typeof AuthRoleCreateIndexRoute
AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute
AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute
} }
const AuthRouteChildren: AuthRouteChildren = { const AuthRouteChildren: AuthRouteChildren = {
AuthAgentIndexRoute: AuthAgentIndexRoute, AuthAgentIndexRoute: AuthAgentIndexRoute,
AuthAppsIndexRoute: AuthAppsIndexRoute, AuthAppsIndexRoute: AuthAppsIndexRoute,
AuthBlacklistIndexRoute: AuthBlacklistIndexRoute, AuthBlacklistsIndexRoute: AuthBlacklistsIndexRoute,
AuthCommandIndexRoute: AuthCommandIndexRoute, AuthCommandsIndexRoute: AuthCommandsIndexRoute,
AuthDashboardIndexRoute: AuthDashboardIndexRoute, AuthDashboardIndexRoute: AuthDashboardIndexRoute,
AuthDeviceIndexRoute: AuthDeviceIndexRoute, AuthDeviceIndexRoute: AuthDeviceIndexRoute,
AuthRoomIndexRoute: AuthRoomIndexRoute, AuthRoleIndexRoute: AuthRoleIndexRoute,
AuthRoomRoomNameIndexRoute: AuthRoomRoomNameIndexRoute, AuthRoomsIndexRoute: AuthRoomsIndexRoute,
AuthRoleCreateIndexRoute: AuthRoleCreateIndexRoute,
AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute,
AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute,
} }
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)

View File

@ -1,21 +1,73 @@
import { createFileRoute, Outlet, redirect } from '@tanstack/react-router' import { useEffect } from "react";
import AppLayout from '@/layouts/app-layout' import AppBreadCrumb from "@/components/app-breadcrumb";
import { AppSidebar } from "@/components/sidebars/app-sidebar";
import AvatarDropdown from "@/components/avatar-dropdown";
import SessionTimeOutErrorPage from "@/components/pages/session-timeout-error";
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar";
import { Separator } from "@/components/ui/separator";
import { createFileRoute, Outlet, redirect, useRouter } from "@tanstack/react-router";
import { useUIStore } from "@/stores/uiStore";
import { useAuth } from "@/hooks/useAuth";
export const Route = createFileRoute('/_auth')({ export const Route = createFileRoute("/_auth")({
beforeLoad: ({ context, location }) => {
// beforeLoad: async ({context}) => { if (!context.auth.isAuthenticated) {
// const {token} = context.auth throw redirect({
// if (!token) { to: "/login",
// throw redirect({to: '/login'}) search: {
// } redirect: location.href
// }, }
component: AuthenticatedLayout, });
}) }
},
function AuthenticatedLayout() { component: RouteComponent
return ( });
<AppLayout>
<Outlet /> function RouteComponent() {
</AppLayout> const auth = useAuth();
) const setCurrent = useUIStore((state) => state.setCurrent);
const router = useRouter();
const navigate = Route.useNavigate();
const currentPath = router.state.location.pathname;
// Update current path in UI store when location changes
useEffect(() => {
setCurrent(currentPath);
}, [currentPath, setCurrent]);
const handleLogout = () => {
if (window.confirm("Bạn chắc chắn muốn đăng xuất khỏi hệ thống?")) {
auth.logout().then(() => {
router.invalidate().finally(() => {
navigate({ to: "/" });
});
});
}
};
const username = auth.username;
if (!auth.isAuthenticated) {
return <SessionTimeOutErrorPage />;
}
return (
<SidebarProvider className="h-screen w-screen">
<AppSidebar />
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
<SidebarTrigger className="-ml-1" />
<Separator orientation="vertical" className="mr-2 h-4" />
<AppBreadCrumb />
<div className="flex items-center gap-4 ml-auto">
<AvatarDropdown username={username} role={auth.role} onLogOut={handleLogout} />
</div>
</header>
<div className="flex flex-1 flex-col gap-4 p-4 overflow-x-auto">
<Outlet />
</div>
</SidebarInset>
</SidebarProvider>
);
} }

View File

@ -10,10 +10,17 @@ import { toast } from "sonner";
import type { ColumnDef } from "@tanstack/react-table"; 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";
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,
errorComponent: ErrorFetchingPage,
loader: async ({ context }) => {
context.breadcrumbs = [
{ title: "Quản lý Agent", path: "/_auth/agent/" },
];
},
}); });
function AgentsPage() { function AgentsPage() {
@ -35,7 +42,7 @@ function AgentsPage() {
const handleUpload = async ( const handleUpload = async (
fd: FormData, fd: FormData,
config?: { onUploadProgress?: (e: AxiosProgressEvent) => void } config?: { onUploadProgress?: (e: AxiosProgressEvent) => void },
) => { ) => {
try { try {
await uploadMutation.mutateAsync({ await uploadMutation.mutateAsync({
@ -54,7 +61,7 @@ function AgentsPage() {
for (const roomName of roomNames) { for (const roomName of roomNames) {
await updateMutation.mutateAsync({ await updateMutation.mutateAsync({
roomName, roomName,
data: {} data: {},
}); });
} }
toast.success("Đã gửi yêu cầu update cho các phòng đã chọn!"); toast.success("Đã gửi yêu cầu update cho các phòng đã chọn!");

View File

@ -20,6 +20,11 @@ import { useState } from "react";
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,
loader: async ({ context }) => {
context.breadcrumbs = [
{ title: "Quản lý phần mềm", path: "/_auth/apps/" },
];
},
}); });
function AppsComponent() { function AppsComponent() {

View File

@ -12,9 +12,14 @@ import { BlackListManagerTemplate } from "@/template/table-manager-template";
import { toast } from "sonner"; import { toast } from "sonner";
import { useState } from "react"; import { useState } from "react";
export const Route = createFileRoute("/_auth/blacklist/")({ export const Route = createFileRoute("/_auth/blacklists/")({
head: () => ({ meta: [{ title: "Danh sách các ứng dụng bị chặn" }] }), head: () => ({ meta: [{ title: "Danh sách các ứng dụng bị chặn" }] }),
component: BlacklistComponent, component: BlacklistComponent,
loader: async ({ context }) => {
context.breadcrumbs = [
{ title: "Quản lý danh sách chặn", path: "/_auth/blacklists/" },
];
},
}); });
function BlacklistComponent() { function BlacklistComponent() {

View File

@ -13,16 +13,19 @@ import {
import { toast } from "sonner"; import { toast } from "sonner";
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 { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import type { ColumnDef } from "@tanstack/react-table"; import type { ColumnDef } from "@tanstack/react-table";
import type { ShellCommandData } from "@/components/forms/command-form"; import type { ShellCommandData } from "@/components/forms/command-form";
import type { CommandRegistry } from "@/types/command-registry"; import type { CommandRegistry } from "@/types/command-registry";
export const Route = createFileRoute("/_auth/command/")({ export const Route = createFileRoute("/_auth/commands/")({
head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }), head: () => ({ meta: [{ title: "Gửi lệnh từ xa" }] }),
component: CommandPage, component: CommandPage,
loader: async ({ context }) => {
context.breadcrumbs = [
{ title: "Quản lý lệnh", path: "/_auth/commands/" },
];
},
}); });
function CommandPage() { function CommandPage() {

View File

@ -2,6 +2,12 @@ import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/_auth/dashboard/')({ export const Route = createFileRoute('/_auth/dashboard/')({
component: RouteComponent, component: RouteComponent,
head: () => ({ meta: [{ title: 'Dashboard' }] }),
loader: async ({ context }) => {
context.breadcrumbs = [
{ title: "Dashboard", path: "/_auth/dashboard/" },
];
},
}) })
function RouteComponent() { function RouteComponent() {

View File

@ -0,0 +1,311 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import {
useGetRoleById,
useGetRolePermissions,
useGetPermissionList,
useToggleRolePermission,
useAssignRolePermissions,
} from "@/hooks/queries";
import { useState, useEffect, useMemo } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Badge } from "@/components/ui/badge";
import { Shield, ArrowLeft, Save, RefreshCw } from "lucide-react";
import { toast } from "sonner";
import type { Permission, PermissionOnRole } from "@/types/permission";
export const Route = createFileRoute("/_auth/role/$id/edit/")({
component: EditRolePermissionsComponent,
loader: async ({ context, params }) => {
context.breadcrumbs = [
{ title: "Quản lý role", path: "/role" },
{ title: "Chỉnh sửa quyền", path: `/role/${params.id}/edit` },
];
},
});
function EditRolePermissionsComponent() {
const navigate = useNavigate();
const { id } = Route.useParams();
const roleId = Number(id);
// Queries
const { data: role, isLoading: roleLoading } = useGetRoleById(roleId);
const { data: rolePermissions = [], isLoading: rolePermissionsLoading } = useGetRolePermissions(roleId);
const { data: allPermissions = [], isLoading: permissionsLoading } = useGetPermissionList();
// Mutations
const toggleMutation = useToggleRolePermission();
const assignMutation = useAssignRolePermissions();
// State
const [selectedPermissions, setSelectedPermissions] = useState<number[]>([]);
const [hasChanges, setHasChanges] = useState(false);
// Initialize selected permissions from role's current permissions
useEffect(() => {
if (rolePermissions && Array.isArray(rolePermissions)) {
// Use permissionEnum as the identifier (matches value from permission list)
const checkedPermissions = rolePermissions
.filter((p: PermissionOnRole) => p.isChecked === 1)
.map((p: PermissionOnRole) => p.permissionEnum);
setSelectedPermissions(checkedPermissions);
}
}, [rolePermissions]);
// Group permissions by parent (category)
const groupedPermissions = useMemo(() => {
const groups: Record<string, Permission[]> = {};
const permissionList = Array.isArray(allPermissions) ? allPermissions : [];
// First pass: identify all parent categories
const parentPermissions: Permission[] = [];
const childPermissions: Permission[] = [];
permissionList.forEach((perm: Permission) => {
const permValue = perm.value ?? perm.enum ?? 0;
const isParent = permValue % 10 === 0;
if (isParent) {
parentPermissions.push(perm);
groups[perm.name] = [];
} else {
childPermissions.push(perm);
}
});
// Second pass: assign children to parent categories
childPermissions.forEach((perm: Permission) => {
const permValue = perm.value ?? perm.enum ?? 0;
const parentEnum = Math.floor(permValue / 10) * 10;
const parent = parentPermissions.find((p: Permission) => (p.value ?? p.enum) === parentEnum);
const parentName = parent?.name || "Khác";
if (!groups[parentName]) {
groups[parentName] = [];
}
groups[parentName].push(perm);
});
// Third pass: add parent permissions that have no children as selectable items
parentPermissions.forEach((parent) => {
const parentValue = parent.value ?? parent.enum ?? 0;
const hasChildren = childPermissions.some((child) => {
const childValue = child.value ?? child.enum ?? 0;
return Math.floor(childValue / 10) * 10 === parentValue;
});
if (!hasChildren) {
groups[parent.name].push(parent);
}
});
// Remove empty groups
Object.keys(groups).forEach((key) => {
if (groups[key].length === 0) {
delete groups[key];
}
});
return groups;
}, [allPermissions]);
// Helper to get unique identifier for permission (use value as ID)
const getPermId = (perm: Permission) => perm.value ?? perm.id ?? 0;
const handleTogglePermission = (permissionValue: number) => {
setSelectedPermissions((prev) =>
prev.includes(permissionValue)
? prev.filter((v) => v !== permissionValue)
: [...prev, permissionValue]
);
setHasChanges(true);
};
const handleSelectAll = (categoryPermissions: Permission[]) => {
const allValues = categoryPermissions.map((p) => getPermId(p));
const allSelected = allValues.every((v) => selectedPermissions.includes(v));
if (allSelected) {
setSelectedPermissions((prev) =>
prev.filter((v) => !allValues.includes(v))
);
} else {
setSelectedPermissions((prev) => [...new Set([...prev, ...allValues])]);
}
setHasChanges(true);
};
// Save all permissions at once
const handleSaveAll = async () => {
try {
await assignMutation.mutateAsync({
roleId,
permissionIds: selectedPermissions,
});
toast.success("Cập nhật quyền thành công!");
setHasChanges(false);
} catch (error) {
toast.error("Cập nhật quyền thất bại!");
}
};
const isLoading = roleLoading || rolePermissionsLoading || permissionsLoading;
if (isLoading) {
return (
<div className="w-full px-6 flex items-center justify-center h-64">
<RefreshCw className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
return (
<div className="w-full px-6 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold flex items-center gap-2">
Chỉnh sửa quyền:
<Badge variant="secondary" className="text-lg">
{role?.roleName}
</Badge>
</h1>
<p className="text-muted-foreground mt-2">
Quản quyền hạn của role này
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => navigate({ to: "/role" })}>
<ArrowLeft className="h-4 w-4 mr-2" />
Quay lại
</Button>
{hasChanges && (
<Button onClick={handleSaveAll} disabled={assignMutation.isPending}>
<Save className="h-4 w-4 mr-2" />
{assignMutation.isPending ? "Đang lưu..." : "Lưu thay đổi"}
</Button>
)}
</div>
</div>
{/* Role Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" /> Thông tin Role
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-4">
<div>
<span className="text-sm text-muted-foreground">Tên Role</span>
<p className="font-medium">{role?.roleName}</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Đ ưu tiên</span>
<p className="font-medium">{role?.priority}</p>
</div>
<div>
<span className="text-sm text-muted-foreground">Số quyền đã gán</span>
<p className="font-medium">{selectedPermissions.length}</p>
</div>
</div>
</CardContent>
</Card>
{/* Permissions */}
<Card>
<CardHeader>
<CardTitle>Quyền hạn</CardTitle>
<CardDescription>
Tick chọn đ bật/tắt quyền ({selectedPermissions.length} đang đưc gán)
</CardDescription>
</CardHeader>
<CardContent>
<ScrollArea className="h-[500px] pr-4">
<div className="space-y-6">
{Object.entries(groupedPermissions).map(([category, perms]) => (
<div key={category} className="space-y-2">
<div className="flex items-center justify-between border-b pb-2">
<h4 className="font-semibold text-sm text-muted-foreground uppercase">
{category}
</h4>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleSelectAll(perms)}
>
{perms.every((p) => selectedPermissions.includes(getPermId(p)))
? "Bỏ tất cả"
: "Chọn tất cả"}
</Button>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{perms.map((perm) => {
const permValue = getPermId(perm);
const isChecked = selectedPermissions.includes(permValue);
return (
<div
key={permValue}
className={`flex items-center space-x-2 p-2 rounded border hover:bg-muted/50 transition-colors ${
isChecked ? "bg-primary/5 border-primary/30" : ""
}`}
>
<Checkbox
id={`perm-${permValue}`}
checked={isChecked}
onCheckedChange={() => handleTogglePermission(permValue)}
disabled={toggleMutation.isPending}
/>
<label
htmlFor={`perm-${permValue}`}
className="text-sm cursor-pointer flex-1"
>
{perm.name}
<span className="text-xs text-muted-foreground ml-1">
({permValue})
</span>
</label>
</div>
);
})}
</div>
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
{/* Footer Actions */}
{hasChanges && (
<div className="flex justify-end gap-2 sticky bottom-4 bg-background p-4 rounded-lg border shadow-lg">
<Button
variant="outline"
onClick={() => {
// Reset to original - use permissionEnum as identifier
const checkedPermissions = (rolePermissions as PermissionOnRole[])
.filter((p) => p.isChecked === 1)
.map((p) => p.permissionEnum);
setSelectedPermissions(checkedPermissions);
setHasChanges(false);
}}
>
Hủy thay đi
</Button>
<Button onClick={handleSaveAll} disabled={assignMutation.isPending}>
<Save className="h-4 w-4 mr-2" />
{assignMutation.isPending ? "Đang lưu..." : "Lưu tất cả thay đổi"}
</Button>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,273 @@
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useGetPermissionList, useCreateRole } from "@/hooks/queries";
import { useState, useMemo } from "react";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Shield, ArrowLeft, Save } from "lucide-react";
import { toast } from "sonner";
import type { Permission } from "@/types/permission";
export const Route = createFileRoute("/_auth/role/create/")({
component: CreateRoleComponent,
loader: async ({ context }) => {
context.breadcrumbs = [
{ title: "Quản lý role", path: "/role" },
{ title: "Tạo role mới", path: "/role/create" },
];
},
});
function CreateRoleComponent() {
const navigate = useNavigate();
const { data: permissions = [], isLoading: permissionsLoading } = useGetPermissionList();
const createMutation = useCreateRole();
const [roleName, setRoleName] = useState("");
const [priority, setPriority] = useState(0);
const [selectedPermissions, setSelectedPermissions] = useState<number[]>([]);
// Helper to get unique identifier for permission
const getPermValue = (perm: Permission) => perm.value ?? perm.id ?? 0;
// Group permissions by parent (category)
const groupedPermissions = useMemo(() => {
const groups: Record<string, Permission[]> = {};
const permissionList = Array.isArray(permissions) ? permissions : [];
// First pass: identify all parent categories
const parentPermissions: Permission[] = [];
const childPermissions: Permission[] = [];
permissionList.forEach((perm: Permission) => {
const permValue = perm.value ?? perm.enum ?? 0;
const isParent = permValue % 10 === 0;
if (isParent) {
parentPermissions.push(perm);
groups[perm.name] = [];
} else {
childPermissions.push(perm);
}
});
// Second pass: assign children to parent categories
childPermissions.forEach((perm: Permission) => {
const permValue = perm.value ?? perm.enum ?? 0;
const parentEnum = Math.floor(permValue / 10) * 10;
const parent = parentPermissions.find((p: Permission) => (p.value ?? p.enum) === parentEnum);
const parentName = parent?.name || "Khác";
if (!groups[parentName]) {
groups[parentName] = [];
}
groups[parentName].push(perm);
});
// Third pass: add parent permissions that have no children as selectable items
// (like ALLOW_ALL which is value 0 with no children)
parentPermissions.forEach((parent) => {
const parentValue = parent.value ?? parent.enum ?? 0;
// Check if this parent has any children
const hasChildren = childPermissions.some((child) => {
const childValue = child.value ?? child.enum ?? 0;
return Math.floor(childValue / 10) * 10 === parentValue;
});
// If no children, add the parent itself as a selectable item
if (!hasChildren) {
groups[parent.name].push(parent);
}
});
// Remove empty groups
Object.keys(groups).forEach((key) => {
if (groups[key].length === 0) {
delete groups[key];
}
});
return groups;
}, [permissions]);
const handleTogglePermission = (permissionValue: number) => {
setSelectedPermissions((prev) =>
prev.includes(permissionValue)
? prev.filter((v) => v !== permissionValue)
: [...prev, permissionValue]
);
};
const handleSelectAll = (categoryPermissions: Permission[]) => {
const allValues = categoryPermissions.map((p) => getPermValue(p));
const allSelected = allValues.every((v) => selectedPermissions.includes(v));
if (allSelected) {
setSelectedPermissions((prev) =>
prev.filter((v) => !allValues.includes(v))
);
} else {
setSelectedPermissions((prev) => [...new Set([...prev, ...allValues])]);
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!roleName.trim()) {
toast.error("Vui lòng nhập tên role!");
return;
}
try {
await createMutation.mutateAsync({
RoleName: roleName,
Priority: priority,
PermissionIds: selectedPermissions,
});
toast.success("Tạo role thành công!");
navigate({ to: "/role" });
} catch (error) {
toast.error("Tạo role thất bại!");
}
};
return (
<div className="w-full px-6 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">Tạo Role mới</h1>
<p className="text-muted-foreground mt-2">
Tạo vai trò mới gán quyền hạn
</p>
</div>
<Button variant="outline" onClick={() => navigate({ to: "/role" })}>
<ArrowLeft className="h-4 w-4 mr-2" />
Quay lại
</Button>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
{/* Role Info */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" /> Thông tin Role
</CardTitle>
<CardDescription>
Nhập thông tin bản của role
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="roleName">Tên Role *</Label>
<Input
id="roleName"
value={roleName}
onChange={(e) => setRoleName(e.target.value)}
placeholder="Nhập tên role..."
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="priority">Đ ưu tiên</Label>
<Input
id="priority"
type="number"
value={priority}
onChange={(e) => setPriority(Number(e.target.value))}
placeholder="0"
/>
</div>
</div>
</CardContent>
</Card>
{/* Permissions Selection */}
<Card>
<CardHeader>
<CardTitle>Chọn quyền hạn</CardTitle>
<CardDescription>
Chọn các quyền role này đưc phép thực hiện ({selectedPermissions.length} đã chọn)
</CardDescription>
</CardHeader>
<CardContent>
{permissionsLoading ? (
<div className="text-center py-4">Đang tải danh sách quyền...</div>
) : (
<ScrollArea className="h-[400px] pr-4">
<div className="space-y-6">
{Object.entries(groupedPermissions).map(([category, perms]) => (
<div key={category} className="space-y-2">
<div className="flex items-center justify-between">
<h4 className="font-semibold text-sm text-muted-foreground uppercase">
{category}
</h4>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => handleSelectAll(perms)}
>
{perms.every((p) => selectedPermissions.includes(getPermValue(p)))
? "Bỏ tất cả"
: "Chọn tất cả"}
</Button>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2">
{perms.map((perm) => {
const permValue = getPermValue(perm);
return (
<div
key={permValue}
className="flex items-center space-x-2 p-2 rounded border hover:bg-muted/50"
>
<Checkbox
id={`perm-${permValue}`}
checked={selectedPermissions.includes(permValue)}
onCheckedChange={() => handleTogglePermission(permValue)}
/>
<Label
htmlFor={`perm-${permValue}`}
className="text-sm cursor-pointer flex-1"
>
{perm.name}
</Label>
</div>
);
})}
</div>
</div>
))}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
{/* Submit */}
<div className="flex justify-end gap-2">
<Button
type="button"
variant="outline"
onClick={() => navigate({ to: "/role" })}
>
Hủy
</Button>
<Button type="submit" disabled={createMutation.isPending}>
<Save className="h-4 w-4 mr-2" />
{createMutation.isPending ? "Đang tạo..." : "Tạo Role"}
</Button>
</div>
</form>
</div>
);
}

View File

@ -0,0 +1,132 @@
import { useGetRoleList, useDeleteRole } from "@/hooks/queries";
import { formatDate } from "@/lib/utils";
import type { TRoleResponse } from "@/types/role";
import { createFileRoute, Link } from "@tanstack/react-router";
import type { ColumnDef } from "@tanstack/react-table";
import { RoleManagerTemplate } from "@/template/role-manager-template";
import { Button } from "@/components/ui/button";
import { Pencil, Trash2, Shield } from "lucide-react";
import { toast } from "sonner";
export const Route = createFileRoute("/_auth/role/")({
component: RoleComponent,
loader: async ({ context }) => {
context.breadcrumbs = [
{
title: "Quản lý role",
path: "#",
},
{
title: "Danh sách role",
path: "/role",
},
];
},
});
function RoleComponent() {
const { data: roles = [], isLoading } = useGetRoleList();
const roleList = Array.isArray(roles) ? roles : [roles];
const deleteMutation = useDeleteRole();
const handleDelete = async (id: number, roleName: string) => {
if (window.confirm(`Bạn có chắc chắn muốn xóa role "${roleName}"?`)) {
try {
await deleteMutation.mutateAsync(id);
toast.success("Xóa role thành công!");
} catch (error) {
toast.error("Xóa role thất bại!");
}
}
};
const columns: ColumnDef<TRoleResponse>[] = [
{
accessorKey: "roleName",
header: () => <div className="font-bold text-center">Role</div>,
cell: ({ row }) => (
<div className="text-center font-medium">{row.original.roleName}</div>
),
},
{
accessorKey: "priority",
header: () => <div className="font-bold text-center">Đ ưu tiên</div>,
cell: ({ row }) => (
<div className="text-center">{row.original.priority}</div>
),
},
{
accessorKey: "createdAt",
header: () => <div className="font-bold text-center">Ngày tạo</div>,
cell: ({ row }) => (
<div className="text-center">
{formatDate(row.original.createdAt)}
</div>
),
},
{
accessorKey: "createdBy",
header: () => <div className="font-bold text-center">Người tạo</div>,
cell: ({ row }) => (
<div className="text-center">{row.original.createdBy || "-"}</div>
),
},
{
accessorKey: "updatedAt",
header: () => <div className="font-bold text-center">Ngày cập nhật</div>,
cell: ({ row }) => (
<div className="text-center">
{formatDate(row.original.updatedAt)}
</div>
),
},
{
accessorKey: "updatedBy",
header: () => <div className="font-bold text-center">Người cập nhật</div>,
cell: ({ row }) => (
<div className="text-center">{row.original.updatedBy || "-"}</div>
),
},
{
id: "actions",
header: () => <div className="font-bold text-center">Hành đng</div>,
cell: ({ row }) => (
<div className="flex items-center justify-center gap-2">
<Link
to="/role/$id/edit"
params={{ id: String(row.original.id) }}
>
<Button variant="outline" size="sm">
<Pencil className="h-4 w-4 mr-1" />
Sửa quyền
</Button>
</Link>
<Button
variant="destructive"
size="sm"
onClick={() => handleDelete(row.original.id, row.original.roleName)}
disabled={deleteMutation.isPending}
>
<Trash2 className="h-4 w-4 mr-1" />
Xóa
</Button>
</div>
),
},
];
return (
<RoleManagerTemplate<TRoleResponse>
title="Quản lý Role"
description="Quản lý các vai trò và quyền hạn trong hệ thống"
data={roleList}
isLoading={isLoading}
columns={columns}
icon={Shield}
tableTitle="Danh sách Role"
tableDescription="Các vai trò trong hệ thống và quyền hạn tương ứng"
createButtonLabel="Tạo role mới"
createLink="/role/create"
/>
);
}

View File

@ -12,7 +12,7 @@ import { useMachineNumber } from "@/hooks/useMachineNumber";
import { toast } from "sonner"; import { toast } from "sonner";
import { CommandActionButtons } from "@/components/buttons/command-action-buttons"; import { CommandActionButtons } from "@/components/buttons/command-action-buttons";
export const Route = createFileRoute("/_auth/room/$roomName/")({ export const Route = createFileRoute("/_auth/rooms/$roomName/")({
head: ({ params }) => ({ head: ({ params }) => ({
meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }], meta: [{ title: `Danh sách thiết bị phòng ${params.roomName}` }],
}), }),
@ -20,7 +20,7 @@ export const Route = createFileRoute("/_auth/room/$roomName/")({
}); });
function RoomDetailPage() { function RoomDetailPage() {
const { roomName } = useParams({ from: "/_auth/room/$roomName/" }); const { roomName } = useParams({ from: "/_auth/rooms/$roomName/" });
const [viewMode, setViewMode] = useState<"grid" | "table">("grid"); const [viewMode, setViewMode] = useState<"grid" | "table">("grid");
const [isCheckingFolder, setIsCheckingFolder] = useState(false); const [isCheckingFolder, setIsCheckingFolder] = useState(false);

View File

@ -31,7 +31,7 @@ import React from "react";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
export const Route = createFileRoute("/_auth/room/")({ export const Route = createFileRoute("/_auth/rooms/")({
head: () => ({ head: () => ({
meta: [{ title: "Danh sách phòng" }], meta: [{ title: "Danh sách phòng" }],
}), }),

View File

@ -1,25 +1,39 @@
import { import { Button } from "@/components/ui/button";
createFileRoute, import { useAuth } from "@/hooks/useAuth";
Outlet, import { createFileRoute, Link, redirect } from "@tanstack/react-router";
} from "@tanstack/react-router";
import AppLayout from "@/layouts/app-layout";
export const Route = createFileRoute("/")({ export const Route = createFileRoute("/")({
head: () => ({ beforeLoad: ({ context }) => {
meta: [ if (!context.auth.isAuthenticated) {
{ throw redirect({
title: "Dashboard", to: "/login",
search: {
redirect: location.pathname
}
});
} else {
throw redirect({ to: "/dashboard" });
}
}, },
], component: Index
}),
component: App,
}); });
function App() { function Index() {
const auth = useAuth();
return ( return (
<> <div className="flex flex-col min-h-[70vh] items-center justify-center text-2xl">
<AppLayout> <h1>Access Control</h1>
<Outlet /> <div className="mt-4">
</AppLayout> {auth.isAuthenticated ? (
</> <Link to="/dashboard">
<Button>Trang chủ</Button>
</Link>
) : (
<Link to="/login">
<Button>Đăng nhập</Button>
</Link>
)}
</div>
</div>
); );
} }

View File

@ -10,3 +10,8 @@ export * as deviceCommService from "./device-comm.service";
// Command API Services // Command API Services
export * as commandService from "./command.service"; export * as commandService from "./command.service";
// Permission API Services
export * as permissionService from "./permission.service";
// Role API Services
export * as roleService from "./role.service";

View File

@ -0,0 +1,71 @@
import axios from "axios";
import { API_ENDPOINTS } from "@/config/api";
import type { Permission } from "@/types/permission";
// Helper to extract data from wrapped or unwrapped response
// Handles both { success, data: T } and { success, data: { data: T, total } }
function extractData<T>(responseData: any): T {
if (responseData && typeof responseData === 'object' && 'success' in responseData && 'data' in responseData) {
const innerData = responseData.data;
// Check for double-wrapped paginated response: { data: [...], total: n }
if (innerData && typeof innerData === 'object' && 'data' in innerData && 'total' in innerData) {
return innerData.data as T;
}
return innerData as T;
}
return responseData as T;
}
/**
* Lấy danh sách permission từ enum
*/
export async function getPermissionList(): Promise<Permission[]> {
const response = await axios.get(API_ENDPOINTS.PERMISSION.GET_LIST);
return extractData<Permission[]>(response.data);
}
/**
* Lấy permission theo category
*/
export async function getPermissionByCategory(): Promise<Record<string, Permission[]>> {
const response = await axios.get(
API_ENDPOINTS.PERMISSION.GET_BY_CATEGORY
);
return extractData<Record<string, Permission[]>>(response.data);
}
/**
* Lấy chi tiết permission theo value
* @param value - Giá trị enum của permission
*/
export async function getPermissionByValue(value: number): Promise<Permission> {
const response = await axios.get(
API_ENDPOINTS.PERMISSION.GET_BY_VALUE(value)
);
return extractData<Permission>(response.data);
}
/**
* Import permission từ enum vào DB (chạy 1 lần đu tiên)
*/
export async function seedPermissionFromEnum(): Promise<any> {
const response = await axios.post(API_ENDPOINTS.PERMISSION.SEED_FROM_ENUM);
return response.data;
}
/**
* Lấy danh sách permission từ database
*/
export async function getPermissionDbList(): Promise<Permission[]> {
const response = await axios.get(API_ENDPOINTS.PERMISSION.GET_DB_LIST);
return extractData<Permission[]>(response.data);
}
/**
* Xóa permission
* @param id - ID của permission
*/
export async function deletePermission(id: number): Promise<any> {
const response = await axios.delete(API_ENDPOINTS.PERMISSION.DELETE(id));
return response.data;
}

View File

@ -0,0 +1,118 @@
import axios from "axios";
import { API_ENDPOINTS } from "@/config/api";
import type { TCreateRoleRequestBody, TRoleResponse } from "@/types/role";
import type { PermissionOnRole } from "@/types/permission";
// Helper to extract data from wrapped or unwrapped response
// Handles both { success, data: T } and { success, data: { data: T, total } }
function extractData<T>(responseData: any): T {
if (responseData && typeof responseData === 'object' && 'success' in responseData && 'data' in responseData) {
const innerData = responseData.data;
// Check for double-wrapped paginated response: { data: [...], total: n }
if (innerData && typeof innerData === 'object' && 'data' in innerData && 'total' in innerData) {
return innerData.data as T;
}
return innerData as T;
}
return responseData as T;
}
/**
* Lấy danh sách tất cả roles
*/
export async function getRoleList(): Promise<TRoleResponse[]> {
const response = await axios.get(API_ENDPOINTS.ROLE.GET_LIST);
return extractData<TRoleResponse[]>(response.data);
}
/**
* Lấy chi tiết role theo ID
* @param id - ID của role
*/
export async function getRoleById(id: number): Promise<TRoleResponse> {
const response = await axios.get(API_ENDPOINTS.ROLE.GET_BY_ID(id));
return extractData<TRoleResponse>(response.data);
}
/**
* Tạo role mới
* @param data - Dữ liệu role mới
*/
export async function createRole(data: TCreateRoleRequestBody): Promise<TRoleResponse> {
const response = await axios.post<TRoleResponse>(API_ENDPOINTS.ROLE.CREATE, data);
return response.data;
}
/**
* Cập nhật role
* @param id - ID của role
* @param data - Dữ liệu cập nhật
*/
export async function updateRole(
id: number,
data: Partial<TCreateRoleRequestBody>
): Promise<TRoleResponse> {
const response = await axios.put<TRoleResponse>(
API_ENDPOINTS.ROLE.UPDATE(id),
data
);
return response.data;
}
/**
* Xóa role
* @param id - ID của role
*/
export async function deleteRole(id: number): Promise<any> {
const response = await axios.delete(API_ENDPOINTS.ROLE.DELETE(id));
return response.data;
}
/**
* Lấy danh sách permissions của role
* @param id - ID của role
*/
export async function getRolePermissions(id: number): Promise<PermissionOnRole[]> {
const response = await axios.get(
API_ENDPOINTS.ROLE.GET_PERMISSIONS(id)
);
// API returns { success, data: { roleId, roleName, permissions: [...] } }
const data = extractData<{ roleId: number; roleName: string; permissions: PermissionOnRole[] }>(response.data);
return data.permissions || [];
}
/**
* Gán permissions cho role (thay thế toàn bộ)
* @param id - ID của role
* @param permissionIds - Danh sách ID permissions
*/
export async function assignRolePermissions(
id: number,
permissionIds: number[]
): Promise<any> {
const response = await axios.post(
API_ENDPOINTS.ROLE.ASSIGN_PERMISSIONS(id),
{ permissionIds }
);
return response.data;
}
/**
* Bật/tắt một permission cụ thể
* @param roleId - ID của role
* @param permissionId - ID của permission
* @param isChecked - Trạng thái bật/tắt
*/
export async function toggleRolePermission(
roleId: number,
permissionId: number,
isChecked: boolean
): Promise<any> {
const response = await axios.patch(
API_ENDPOINTS.ROLE.TOGGLE_PERMISSION(roleId, permissionId),
null,
{ params: { isChecked } }
);
return response.data;
}

48
src/stores/uiStore.ts Normal file
View File

@ -0,0 +1,48 @@
import { appSidebarSection } from "@/types/app-sidebar";
import { create } from "zustand";
interface UIState {
isSidebarCollapsed: boolean;
current: number;
setCurrent: (value: number | string) => void;
getCurrentPath: (current: number) => string[];
toggleSidebar: () => void;
}
const data = appSidebarSection;
export const useUIStore = create<UIState>((set, get) => ({
isSidebarCollapsed: true,
toggleSidebar: () => {
set({ isSidebarCollapsed: !get().isSidebarCollapsed });
},
current: localStorage.getItem("current") ? parseInt(localStorage.getItem("current")!) : 0,
setCurrent: (value: number | string) => {
if (typeof value === "string") {
for (const item of data.navMain) {
for (const subItem of item.items) {
if (subItem.url === value) {
if (subItem.code !== undefined) {
value = subItem.code;
}
break;
}
}
}
if (typeof value === "string") {
value = 0;
}
}
localStorage.setItem("current", value.toString());
set({ current: value });
},
getCurrentPath: (current: number) => {
for (const section of data.navMain) {
const item = section.items.find((item) => item.code === current);
if (item) {
return [section.title, item.title];
}
}
return [];
}
}));

View File

@ -0,0 +1,87 @@
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { VersionTable } from "@/components/tables/version-table";
import type { ColumnDef } from "@tanstack/react-table";
import { Plus, Shield, type LucideIcon } from "lucide-react";
import { Link } from "@tanstack/react-router";
import type { ReactNode } from "react";
interface RoleManagerTemplateProps<TData> {
title: string;
description: string;
data: TData[];
isLoading: boolean;
columns: ColumnDef<TData, any>[];
onTableInit?: (table: any) => void;
// Optional customization
icon?: LucideIcon;
tableTitle?: string;
tableDescription?: string;
createButtonLabel?: string;
createLink?: string;
headerActions?: ReactNode;
}
export function RoleManagerTemplate<TData>({
title,
description,
data,
isLoading,
columns,
onTableInit,
icon: Icon = Shield,
tableTitle = "Danh sách",
tableDescription,
createButtonLabel = "Tạo mới",
createLink,
headerActions,
}: RoleManagerTemplateProps<TData>) {
return (
<div className="w-full px-6 space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold">{title}</h1>
<p className="text-muted-foreground mt-2">{description}</p>
</div>
<div className="flex items-center gap-2">
{headerActions}
{createLink && (
<Link to={createLink}>
<Button>
<Plus className="h-4 w-4 mr-2" />
{createButtonLabel}
</Button>
</Link>
)}
</div>
</div>
{/* Table */}
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Icon className="h-5 w-5" /> {tableTitle}
</CardTitle>
{tableDescription && (
<CardDescription>{tableDescription}</CardDescription>
)}
</CardHeader>
<CardContent>
<VersionTable
data={data}
isLoading={isLoading}
columns={columns}
onTableInit={onTableInit}
/>
</CardContent>
</Card>
</div>
);
}

View File

@ -1,59 +1,92 @@
import { AppWindow, Building, CircleX, Home, Terminal } from "lucide-react"; import { AppWindow, Building, CircleX, Home, ShieldCheck, Terminal} from "lucide-react";
import { PermissionEnum } from "./permission"; import { PermissionEnum } from "./permission";
enum AppSidebarSectionCode { enum AppSidebarSectionCode {
DASHBOARD = 1, DASHBOARD = 1,
DEVICES = 2, ROOM_LIST = 2,
DOOR = 4, AGENT_MANAGEMENT = 3,
DOOR_LAYOUT = 5, APP_MANAGEMENT = 4,
BUILDING_DASHBOARD = 6, COMMAND_SENDER = 5,
SETUP_DOOR = 7, BLACKLIST = 6,
DOOR_STATUS = 8, ROOM_DETAIL = 7,
DEPARTMENTS = 9, LIST_ROLES = 8,
DEPARTMENT_PATHS = 10, LIST_PERMISSIONS = 9,
SCHEDULES = 11, LIST_USERS = 10,
ACCESS_STATUS = 12,
ACCESS_HISTORY = 13,
CONFIG_MANAGER,
APP_VERSION_MANAGER,
DEVICES_APP_VERSION,
HEALTHCHEAK,
LIST_ROLES,
ACCOUNT_PERMISSION,
LIST_ACCOUNT,
DOOR_WARNING,
COMMAND_HISTORY,
ACCESS_ILLEGAL,
ZONES,
MANTRAP,
ROLES,
DEVICES_SYNC_BIO,
} }
export const appSidebarSection = { export const appSidebarSection = {
versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"], versions: ["1.0.1", "1.1.0-alpha", "2.0.0-beta1"],
navMain: [ navMain: [
{ title: "Dashboard", to: "/", icon: Home },
{ {
title: "Danh sách phòng", title: "Thống kê tổng quan",
to: "/room", items: [
icon: Building, {
title: "Dashboard",
url: "/dashboard",
code: AppSidebarSectionCode.DASHBOARD,
icon: Home,
permissions: [PermissionEnum.ALLOW_ALL],
},
],
}, },
{ {
title: "Quản lý Agent", title: "Quan lý phòng máy",
to: "/agent", items: [
{
title: "Danh sách phòng máy",
url: "/rooms",
code: AppSidebarSectionCode.ROOM_LIST,
icon: Building,
permissions: [PermissionEnum.VIEW_ROOM],
},
],
},
{
title: "Agent và phần mềm",
items: [
{
title: "Danh sách Agent",
url: "/agent",
code: AppSidebarSectionCode.AGENT_MANAGEMENT,
icon: AppWindow, icon: AppWindow,
permissions: [PermissionEnum.VIEW_AGENT],
}, },
{ {
title: "Quản lý phần mềm", title: "Quản lý phần mềm",
to: "/apps", url: "/apps",
icon: AppWindow, icon: AppWindow,
permissions: [PermissionEnum.VIEW_APPS],
}
],
}, },
{ title: "Gửi lệnh CMD", to: "/command", icon: Terminal },
{ {
title: "Danh sách đen", title: "Lệnh và các ứng dụng bị chặn",
to: "/blacklist", items:
icon: CircleX, [
{
title: "Gửi lệnh từ xa",
url: "/commands",
icon: Terminal,
permissions: [PermissionEnum.VIEW_COMMAND],
}, },
{
title: "Danh sách ứng dụng/web bị chặn",
url: "/blacklists",
icon: CircleX,
permissions: [PermissionEnum.ALLOW_ALL],
}
]
},
{
title: "Phân quyền và người dùng",
items: [
{
title: "Danh sách roles",
url: "/role",
icon: ShieldCheck,
permissions: [PermissionEnum.VIEW_ROLES],
}
]
}
], ],
}; };

View File

@ -1,9 +1,10 @@
export type Permission = { export type Permission = {
id: number; id?: number; // From DB API
name: string; name: string;
code: string; code: string;
parentId: number | null; value: number; // Enum value from API
enum: PermissionEnum; parentId?: number | null;
enum?: PermissionEnum; // Deprecated, use value
}; };
export type PermissionOnRole = { export type PermissionOnRole = {
@ -29,31 +30,20 @@ export enum PermissionEnum {
EDIT_APP_CONFIG = 23, EDIT_APP_CONFIG = 23,
DEL_APP_CONFIG = 24, DEL_APP_CONFIG = 24,
//BIOMETRIC_OPERATION //ROOM_OPERATION
BIOMETRIC_OPERATION = 30,
VIEW_GUEST = 31,
GET_BIO = 32,
GET_SEND_BIO_STATUS = 33,
//BUILDING_OPERATION
BUILDING_OPERATION = 40, BUILDING_OPERATION = 40,
VIEW_BUILDING = 41, VIEW_ROOM = 41,
CREATE_BUILDING = 42, CREATE_ROOM = 42,
EDIT_BUILDING = 43, EDIT_ROOM = 43,
CREATE_LV = 45, DEL_ROOM = 44,
DEL_BUILDING = 44,
//COMMAND_OPERATION //COMMAND_OPERATION
COMMAND_OPERATION = 50, COMMAND_OPERATION = 50,
VIEW_COMMAND = 51, VIEW_COMMAND = 51,
CREATE_COMMAND = 52,
//DEPARTMENT_OPERATION EDIT_COMMAND = 53,
DEPARTMENT_OPERATION = 60, DEL_COMMAND = 54,
VIEW_DEP = 61, SEND_COMMAND = 55,
CREATE_DEP = 62,
EDIT_DEP = 63,
DEL_DEP = 64,
VIEW_PATH = 65,
//DEVICE_OPERATION //DEVICE_OPERATION
DEVICE_OPERATION = 70, DEVICE_OPERATION = 70,
@ -61,55 +51,13 @@ export enum PermissionEnum {
EDIT_DEVICE = 73, EDIT_DEVICE = 73,
VIEW_DEVICE = 74, VIEW_DEVICE = 74,
//DOOR_OPERATION
DOOR_OPERATION = 80,
SET_DOOR_POSITION = 85,
RESET_DOOR_POSITION = 86,
VIEW_DOOR = 81,
ADD_DOOR = 82,
EDIT_DOOR = 83,
DEL_DOOR = 84,
ADD_DEVICE_TO_DOOR = 87,
REMOVE_DEVICE_FROM_DOOR = 88,
SEND_COMMAND = 801,
SEND_EMERGENCY = 803,
CONTROL_DOOR = 805,
//LEVEL_OPERATION
LEVEL_OPERATION = 90,
UPLOAD_LAYOUT = 91,
VIEW_LEVEL_IN_BUILDING = 92,
EDIT_LV = 93,
DEL_LV = 94,
//PATH_OPERATION
PATH_OPERATION = 100,
CREATE_PATH = 102,
EDIT_PATH = 103,
DEL_PATH = 104,
//PERMISSION_OPERATION //PERMISSION_OPERATION
PERMISSION_OPERATION = 110, PERMISSION_OPERATION = 110,
VIEW_ALL_PER = 111, VIEW_ALL_PER = 111,
CRE_PER = 112, CRE_PER = 112,
DEL_PER = 114, DEL_PER = 114,
VIEW_ACCOUNT_BUILDING = 115, VIEW_ACCOUNT_ROOM = 115,
EDIT_ACCOUNT_BUILDING = 116, EDIT_ACCOUNT_ROOM = 116,
//ZONE_OPERATION
ZONE_OPERATION = 120,
CREATE_ZONE = 122,
EDIT_ZONE = 123,
DEL_ZONE = 124,
VIEW_ZONE = 121,
//SCHEDULE_OPERATION
SCHEDULE_OPERATION = 130,
DEL_SCHEDULE = 134,
CREATE_SCHEDULE = 132,
EDIT_SCHEDULE = 133,
VIEW_ALL_SCHEDULE = 131,
//WARNING_OPERATION //WARNING_OPERATION
WARNING_OPERATION = 140, WARNING_OPERATION = 140,
@ -121,6 +69,7 @@ export enum PermissionEnum {
VIEW_USER = 152, VIEW_USER = 152,
EDIT_USER_ROLE = 153, EDIT_USER_ROLE = 153,
CRE_USER = 154, CRE_USER = 154,
CHANGE_PASSWORD = 155,
//ROLE_OPERATION //ROLE_OPERATION
ROLE_OPERATION = 160, ROLE_OPERATION = 160,
@ -130,15 +79,24 @@ export enum PermissionEnum {
EDIT_ROLE_PER = 163, EDIT_ROLE_PER = 163,
DEL_ROLE = 164, DEL_ROLE = 164,
// APP VERSION // AGENT
APP_VERSION_OPERATION = 170, APP_OPERATION = 170,
VIEW_APP_VERSION = 171, VIEW_AGENT = 171,
UPLOAD_APK = 172, UPDATE_AGENT = 173,
SEND_UPDATE_COMMAND = 174,
CHANGE_PASSWORD = 2, // APPS
APPS_OPERATION = 180,
VIEW_APPS = 181,
CREATE_APP = 182,
EDIT_APP = 183,
DEL_APP = 184,
ADD_APP_TO_SELECTED = 185,
DEL_APP_FROM_SELECTED = 186,
//Undefined //Undefined
UNDEFINED = 9999, UNDEFINED = 9999,
ALLOW_ALL = 0 //Allow All
ALLOW_ALL = 0,
} }

15
src/types/role.ts Normal file
View File

@ -0,0 +1,15 @@
export type TCreateRoleRequestBody = {
RoleName: string;
Priority: number;
PermissionIds: number[];
};
export type TRoleResponse = {
id: number;
roleName: string;
priority: number;
createdAt: Date | null;
updatedAt: Date | null;
createdBy: string | null;
updatedBy: string | null;
};