diff --git a/SYSTEM_ADMIN_PRIORITY_GUIDE.md b/SYSTEM_ADMIN_PRIORITY_GUIDE.md new file mode 100644 index 0000000..03211d2 --- /dev/null +++ b/SYSTEM_ADMIN_PRIORITY_GUIDE.md @@ -0,0 +1,249 @@ +# System Admin Priority Logic - Hướng dẫn + +## Tổng quan + +Đã cập nhật logic để **System Admin** (Priority = 0) trở thành quyền cao nhất trong hệ thống. + +### Quy tắc Priority + +``` +Priority càng thấp = Quyền càng cao +Priority = 0 (System Admin) = Quyền cao nhất +``` + +## Các thay đổi đã thực hiện + +### 1. Constants mới (`src/config/constants.ts`) + +```typescript +export const SYSTEM_ADMIN_PRIORITY = 0; + +export const RolePriority = { + SYSTEM_ADMIN: 0, +} as const; +``` + +**Mục đích**: Định nghĩa giá trị priority của System Admin, tránh hardcode số 0 trong code. + +--- + +### 2. Helper Functions (`src/helpers/roleHelpers.ts`) + +#### `isSystemAdminPriority(priority: number): boolean` +Kiểm tra xem priority có phải là System Admin không. + +```typescript +import { isSystemAdminPriority } from '@/helpers/roleHelpers'; + +if (isSystemAdminPriority(userPriority)) { + // User là System Admin +} +``` + +#### `hasHigherOrEqualPriority(priority1, priority2): boolean` +So sánh 2 priority (nhỏ hơn = cao hơn). + +```typescript +if (hasHigherOrEqualPriority(userPriority, requiredPriority)) { + // User có đủ quyền +} +``` + +#### `getPriorityLabel(priority: number): string` +Lấy nhãn mô tả cho priority. + +```typescript +getPriorityLabel(0) // "System Admin (Highest)" +getPriorityLabel(5) // "Priority 5" +``` + +--- + +### 3. useAuth Hook (`src/hooks/useAuth.tsx`) + +Thêm method mới: `isSystemAdmin()` + +```typescript +const { isSystemAdmin } = useAuth(); + +if (isSystemAdmin()) { + // User là System Admin (priority = 0) + console.log('You have highest permission!'); +} +``` + +**Interface cập nhật:** +```typescript +export interface IAuthContext { + // ... các field cũ + isSystemAdmin: () => boolean; // ← Mới +} +``` + +--- + +### 4. Sidebar Logic (`src/components/sidebars/app-sidebar.tsx`) + +Cập nhật logic kiểm tra admin: + +```typescript +// TRƯỚC +const isAdmin = acs.includes(PermissionEnum.ALLOW_ALL); + +// SAU +const isAdmin = acs.includes(PermissionEnum.ALLOW_ALL) || isSystemAdmin(); +``` + +**Lợi ích:** +- System Admin (Priority = 0) thấy tất cả menu items +- Không cần phải có permission `ALLOW_ALL` trong database + +--- + +## Cách sử dụng + +### Kiểm tra System Admin trong component + +```typescript +import { useAuth } from '@/hooks/useAuth'; + +function MyComponent() { + const { isSystemAdmin, role } = useAuth(); + + return ( +
+ {isSystemAdmin() && ( + + )} + +

Role: {role.roleName}

+

Priority: {role.priority}

+
+ ); +} +``` + +### Kiểm tra priority trong logic nghiệp vụ + +```typescript +import { isSystemAdminPriority, hasHigherOrEqualPriority } from '@/helpers/roleHelpers'; + +function canDeleteUser(currentUserPriority: number, targetUserPriority: number): boolean { + // System Admin có thể xóa bất kỳ ai + if (isSystemAdminPriority(currentUserPriority)) { + return true; + } + + // User chỉ có thể xóa user có priority thấp hơn (số lớn hơn) + return hasHigherOrEqualPriority(currentUserPriority, targetUserPriority); +} +``` + +### Hiển thị label priority + +```typescript +import { getPriorityLabel } from '@/helpers/roleHelpers'; + +{getPriorityLabel(role.priority)} +// System Admin sẽ hiển thị: "System Admin (Highest)" +``` + +--- + +## Luồng kiểm tra quyền + +``` +User đăng nhập + ↓ +Priority được lưu vào localStorage + ↓ +useAuth hook load priority + ↓ +isSystemAdmin() kiểm tra priority === 0 + ↓ +Sidebar check: ALLOW_ALL || isSystemAdmin() + ↓ +Hiển thị menu items phù hợp +``` + +--- + +## Ví dụ thực tế + +### Ví dụ 1: Ẩn/hiện nút Delete dựa trên priority + +```typescript +function UserManagement() { + const { role, isSystemAdmin } = useAuth(); + const currentUserPriority = role.priority; + + function canDelete(targetUserPriority: number): boolean { + // System Admin xóa được tất cả + if (isSystemAdmin()) return true; + + // Priority thấp hơn (số nhỏ hơn) mới xóa được + return currentUserPriority < targetUserPriority; + } + + return ( + + {users.map(user => ( + + {user.name} + + {canDelete(user.role.priority) && ( + + )} + + + ))} +
+ ); +} +``` + +### Ví dụ 2: Route protection + +```typescript +import { useAuth } from '@/hooks/useAuth'; +import { redirect } from '@tanstack/react-router'; + +export const Route = createFileRoute('/_auth/admin-panel')({ + beforeLoad: ({ context }) => { + const { isSystemAdmin } = context.auth; + + if (!isSystemAdmin()) { + throw redirect({ + to: '/unauthorized', + }); + } + }, + component: AdminPanel, +}); +``` + +--- + +## Tóm tắt + +✅ **Priority = 0** là System Admin (quyền cao nhất) +✅ **Priority thấp hơn** = Quyền cao hơn +✅ Có constants và helpers để tái sử dụng +✅ `isSystemAdmin()` method trong useAuth hook +✅ Sidebar tự động nhận biết System Admin +✅ Không cần hardcode giá trị priority nữa + +--- + +## Files đã thay đổi + +1. ✅ `src/config/constants.ts` - Constants mới +2. ✅ `src/helpers/roleHelpers.ts` - Helper functions +3. ✅ `src/hooks/useAuth.tsx` - Thêm isSystemAdmin() +4. ✅ `src/types/auth.ts` - Cập nhật interface +5. ✅ `src/components/sidebars/app-sidebar.tsx` - Logic admin check + +--- + +**Lưu ý quan trọng:** +Backend cũng cần implement logic tương tự để đảm bảo consistency giữa frontend và backend! diff --git a/src/components/avatar-dropdown.tsx b/src/components/avatar-dropdown.tsx index 91f929c..763112c 100644 --- a/src/components/avatar-dropdown.tsx +++ b/src/components/avatar-dropdown.tsx @@ -7,7 +7,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; -import { LogOut, Settings, User } from "lucide-react"; +import { LogOut, Settings, User, Key } from "lucide-react"; interface AvatarDropdownProps { username: string; @@ -18,6 +18,7 @@ interface AvatarDropdownProps { onLogOut: () => void; onSettings?: () => void; onProfile?: () => void; + onChangePassword?: () => void; } export default function AvatarDropdown({ @@ -26,6 +27,7 @@ export default function AvatarDropdown({ onLogOut, onSettings, onProfile, + onChangePassword, }: AvatarDropdownProps) { // Get initials from username const getInitials = (name: string): string => { @@ -62,7 +64,13 @@ export default function AvatarDropdown({ {onProfile && ( - Tài khoản của tôi + Thông tin cá nhân + + )} + {onChangePassword && ( + + + Đổi mật khẩu )} {onSettings && ( @@ -71,7 +79,7 @@ export default function AvatarDropdown({ Cài đặt )} - {(onProfile || onSettings) && } + {(onProfile || onChangePassword || onSettings) && } void; // callback để refresh danh sách sau khi thêm diff --git a/src/components/pagination/pagination.tsx b/src/components/pagination/pagination.tsx index 4081f81..a68df13 100644 --- a/src/components/pagination/pagination.tsx +++ b/src/components/pagination/pagination.tsx @@ -21,12 +21,8 @@ export function CustomPagination({ onPageSizeChange, pageSizeOptions }: PaginationProps) { - let startItem = (currentPage - 1) * itemsPerPage + 1; - let endItem = Math.min(currentPage * itemsPerPage, totalItems); - if (currentPage === totalPages) { - startItem = totalItems - itemsPerPage + 1; - endItem = totalItems; - } + const startItem = Math.max(1, (currentPage - 1) * itemsPerPage + 1); + const endItem = Math.min(currentPage * itemsPerPage, totalItems); return (
diff --git a/src/components/sidebars/app-sidebar.tsx b/src/components/sidebars/app-sidebar.tsx index 13503be..b93e07e 100644 --- a/src/components/sidebars/app-sidebar.tsx +++ b/src/components/sidebars/app-sidebar.tsx @@ -13,6 +13,12 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import { cn } from "@/lib/utils"; import { appSidebarSection } from "@/types/app-sidebar"; import { PermissionEnum } from "@/types/permission"; @@ -33,10 +39,10 @@ type SidebarSection = { }; export function AppSidebar() { - const { hasPermission, acs } = useAuth(); + const { hasPermission, acs, isSystemAdmin } = useAuth(); - // Check if user is admin (has ALLOW_ALL permission) - const isAdmin = acs.includes(PermissionEnum.ALLOW_ALL); + // Check if user is admin (has ALLOW_ALL permission OR is System Admin with priority 0) + const isAdmin = acs.includes(PermissionEnum.ALLOW_ALL) || isSystemAdmin(); // Check if user has any of the required permissions const checkPermissions = (permissions?: PermissionEnum[]) => { @@ -44,7 +50,7 @@ export function AppSidebar() { 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 + // Admin users OR System Admin (priority=0) see everything if (isAdmin) return true; // Check if user has any of the required permissions return permissions.some((permission) => hasPermission(permission)); @@ -62,10 +68,11 @@ export function AppSidebar() { }, [acs]); return ( - + +
@@ -93,29 +100,36 @@ export function AppSidebar() { {section.items.map((item) => ( - - - - - {item.title} - - - + + + + + + + {item.title} + + + + + + {item.title} + + ))} @@ -130,5 +144,6 @@ export function AppSidebar() {
+ ); } diff --git a/src/config/api.ts b/src/config/api.ts index 0df2e60..fd6cc56 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -61,47 +61,21 @@ export const API_ENDPOINTS = { 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`, }, diff --git a/src/config/axios.ts b/src/config/axios.ts new file mode 100644 index 0000000..84f84a4 --- /dev/null +++ b/src/config/axios.ts @@ -0,0 +1,49 @@ +import axios from "axios"; + +// Re-export types from axios for convenience +export type { AxiosProgressEvent, AxiosRequestConfig, AxiosResponse, AxiosError } from "axios"; + +/** + * Axios instance với interceptor tự động gửi token + */ +const axiosInstance = axios.create(); + +/** + * Request interceptor - Tự động thêm Authorization header + */ +axiosInstance.interceptors.request.use( + (config) => { + // Lấy token từ localStorage + const token = localStorage.getItem("token"); + + // Nếu có token, thêm vào header Authorization + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +/** + * Response interceptor - Xử lý lỗi 401 (Unauthorized) + */ +axiosInstance.interceptors.response.use( + (response) => { + return response; + }, + (error) => { + // Nếu nhận được 401, có thể redirect về trang login + if (error.response?.status === 401) { + // Có thể thêm logic redirect hoặc refresh token ở đây + console.warn("Unauthorized - Token may be expired or invalid"); + } + + return Promise.reject(error); + } +); + +export default axiosInstance; diff --git a/src/config/constants.ts b/src/config/constants.ts new file mode 100644 index 0000000..fa893c6 --- /dev/null +++ b/src/config/constants.ts @@ -0,0 +1,10 @@ +/** + * System-wide constants + */ + +/** + * System Admin priority value + * Priority = 0 means highest permission level (System Admin) + * Lower priority number = Higher permission level + */ +export const SYSTEM_ADMIN_PRIORITY = 0; diff --git a/src/helpers/roleHelpers.ts b/src/helpers/roleHelpers.ts new file mode 100644 index 0000000..5307812 --- /dev/null +++ b/src/helpers/roleHelpers.ts @@ -0,0 +1,45 @@ +import { SYSTEM_ADMIN_PRIORITY } from "@/config/constants"; + +/** + * Check if a priority value indicates System Admin + * @param priority - The priority value to check + * @returns true if the priority is System Admin (0), false otherwise + */ +export function isSystemAdminPriority(priority: number): boolean { + return priority === SYSTEM_ADMIN_PRIORITY; +} + +/** + * Check if a priority has higher permission than another + * Lower number = Higher permission (System Admin = 0 is highest) + * @param priority1 - First priority to compare + * @param priority2 - Second priority to compare + * @returns true if priority1 has higher or equal permission than priority2 + */ +export function hasHigherOrEqualPriority(priority1: number, priority2: number): boolean { + return priority1 <= priority2; +} + +/** + * Compare two priorities + * @param priority1 - First priority to compare + * @param priority2 - Second priority to compare + * @returns -1 if priority1 > priority2, 0 if equal, 1 if priority1 < priority2 + */ +export function comparePriorities(priority1: number, priority2: number): -1 | 0 | 1 { + if (priority1 < priority2) return 1; // Lower number = higher permission + if (priority1 > priority2) return -1; + return 0; +} + +/** + * Get a human-readable priority label + * @param priority - The priority value + * @returns A label for the priority + */ +export function getPriorityLabel(priority: number): string { + if (priority === SYSTEM_ADMIN_PRIORITY) { + return "System Admin (Highest)"; + } + return `Priority ${priority}`; +} diff --git a/src/hooks/queries/useAuthQueries.ts b/src/hooks/queries/useAuthQueries.ts index a81e519..4636fc8 100644 --- a/src/hooks/queries/useAuthQueries.ts +++ b/src/hooks/queries/useAuthQueries.ts @@ -1,5 +1,6 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import * as authService from "@/services/auth.service"; +import type { CreateAccountRequest } from "@/types/auth"; import type { LoginResquest, LoginResponse } from "@/types/auth"; const AUTH_QUERY_KEYS = { @@ -105,13 +106,7 @@ export function useCreateAccount() { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (data: { - userName: string; - password: string; - name: string; - roleId: number; - accessBuildings?: number[]; - }) => authService.createAccount(data), + mutationFn: (data: CreateAccountRequest) => authService.createAccount(data), onSuccess: () => { // Có thể invalidate user list query nếu có queryClient.invalidateQueries({ queryKey: ["users"] }); diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 8a5c180..c941554 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -1,5 +1,6 @@ import { sleep } from "@/lib/utils"; import { PermissionEnum } from "@/types/permission"; +import { SYSTEM_ADMIN_PRIORITY } from "@/config/constants"; import React, { useContext, useEffect, useMemo } from "react"; import { useCallback, useState } from "react"; @@ -13,6 +14,7 @@ export interface IAuthContext { name: string; acs: number[]; hasPermission: (permission: PermissionEnum) => boolean; + isSystemAdmin: () => boolean; role: { roleName: string; priority: number; @@ -61,6 +63,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { [acs] ); + const isSystemAdmin = useCallback(() => { + return Number(priority) === SYSTEM_ADMIN_PRIORITY; + }, [priority]); + const logout = useCallback(async () => { await sleep(250); setAuthenticated(false); @@ -89,7 +95,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { name, acs, role: { roleName, priority: Number(priority) }, - hasPermission + hasPermission, + isSystemAdmin }}> {children} diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index ea658a9..b7464ab 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -20,8 +20,14 @@ import { Route as AuthBlacklistsIndexRouteImport } from './routes/_auth/blacklis import { Route as AuthAppsIndexRouteImport } from './routes/_auth/apps/index' import { Route as AuthAgentIndexRouteImport } from './routes/_auth/agent/index' import { Route as authLoginIndexRouteImport } from './routes/(auth)/login/index' +import { Route as AuthUserRoleIndexRouteImport } from './routes/_auth/user/role/index' +import { Route as AuthUserCreateIndexRouteImport } from './routes/_auth/user/create/index' import { Route as AuthRoomsRoomNameIndexRouteImport } from './routes/_auth/rooms/$roomName/index' import { Route as AuthRoleCreateIndexRouteImport } from './routes/_auth/role/create/index' +import { Route as AuthProfileChangePasswordIndexRouteImport } from './routes/_auth/profile/change-password/index' +import { Route as AuthProfileUserNameIndexRouteImport } from './routes/_auth/profile/$userName/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 AuthRoleIdEditIndexRouteImport } from './routes/_auth/role/$id/edit/index' const AuthRoute = AuthRouteImport.update({ @@ -78,6 +84,16 @@ const authLoginIndexRoute = authLoginIndexRouteImport.update({ path: '/login/', getParentRoute: () => rootRouteImport, } as any) +const AuthUserRoleIndexRoute = AuthUserRoleIndexRouteImport.update({ + id: '/user/role/', + path: '/user/role/', + getParentRoute: () => AuthRoute, +} as any) +const AuthUserCreateIndexRoute = AuthUserCreateIndexRouteImport.update({ + id: '/user/create/', + path: '/user/create/', + getParentRoute: () => AuthRoute, +} as any) const AuthRoomsRoomNameIndexRoute = AuthRoomsRoomNameIndexRouteImport.update({ id: '/rooms/$roomName/', path: '/rooms/$roomName/', @@ -88,6 +104,29 @@ const AuthRoleCreateIndexRoute = AuthRoleCreateIndexRouteImport.update({ path: '/role/create/', getParentRoute: () => AuthRoute, } as any) +const AuthProfileChangePasswordIndexRoute = + AuthProfileChangePasswordIndexRouteImport.update({ + id: '/profile/change-password/', + path: '/profile/change-password/', + getParentRoute: () => AuthRoute, + } as any) +const AuthProfileUserNameIndexRoute = + AuthProfileUserNameIndexRouteImport.update({ + id: '/profile/$userName/', + path: '/profile/$userName/', + getParentRoute: () => AuthRoute, + } as any) +const AuthUserRoleRoleIdIndexRoute = AuthUserRoleRoleIdIndexRouteImport.update({ + id: '/user/role/$roleId/', + path: '/user/role/$roleId/', + getParentRoute: () => AuthRoute, +} as any) +const AuthUserChangePasswordUserNameIndexRoute = + AuthUserChangePasswordUserNameIndexRouteImport.update({ + id: '/user/change-password/$userName/', + path: '/user/change-password/$userName/', + getParentRoute: () => AuthRoute, + } as any) const AuthRoleIdEditIndexRoute = AuthRoleIdEditIndexRouteImport.update({ id: '/role/$id/edit/', path: '/role/$id/edit/', @@ -105,9 +144,15 @@ export interface FileRoutesByFullPath { '/device': typeof AuthDeviceIndexRoute '/role': typeof AuthRoleIndexRoute '/rooms': typeof AuthRoomsIndexRoute + '/profile/$userName': typeof AuthProfileUserNameIndexRoute + '/profile/change-password': typeof AuthProfileChangePasswordIndexRoute '/role/create': typeof AuthRoleCreateIndexRoute '/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute + '/user/create': typeof AuthUserCreateIndexRoute + '/user/role': typeof AuthUserRoleIndexRoute '/role/$id/edit': typeof AuthRoleIdEditIndexRoute + '/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute + '/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -120,9 +165,15 @@ export interface FileRoutesByTo { '/device': typeof AuthDeviceIndexRoute '/role': typeof AuthRoleIndexRoute '/rooms': typeof AuthRoomsIndexRoute + '/profile/$userName': typeof AuthProfileUserNameIndexRoute + '/profile/change-password': typeof AuthProfileChangePasswordIndexRoute '/role/create': typeof AuthRoleCreateIndexRoute '/rooms/$roomName': typeof AuthRoomsRoomNameIndexRoute + '/user/create': typeof AuthUserCreateIndexRoute + '/user/role': typeof AuthUserRoleIndexRoute '/role/$id/edit': typeof AuthRoleIdEditIndexRoute + '/user/change-password/$userName': typeof AuthUserChangePasswordUserNameIndexRoute + '/user/role/$roleId': typeof AuthUserRoleRoleIdIndexRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -137,9 +188,15 @@ export interface FileRoutesById { '/_auth/device/': typeof AuthDeviceIndexRoute '/_auth/role/': typeof AuthRoleIndexRoute '/_auth/rooms/': typeof AuthRoomsIndexRoute + '/_auth/profile/$userName/': typeof AuthProfileUserNameIndexRoute + '/_auth/profile/change-password/': typeof AuthProfileChangePasswordIndexRoute '/_auth/role/create/': typeof AuthRoleCreateIndexRoute '/_auth/rooms/$roomName/': typeof AuthRoomsRoomNameIndexRoute + '/_auth/user/create/': typeof AuthUserCreateIndexRoute + '/_auth/user/role/': typeof AuthUserRoleIndexRoute '/_auth/role/$id/edit/': typeof AuthRoleIdEditIndexRoute + '/_auth/user/change-password/$userName/': typeof AuthUserChangePasswordUserNameIndexRoute + '/_auth/user/role/$roleId/': typeof AuthUserRoleRoleIdIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -154,9 +211,15 @@ export interface FileRouteTypes { | '/device' | '/role' | '/rooms' + | '/profile/$userName' + | '/profile/change-password' | '/role/create' | '/rooms/$roomName' + | '/user/create' + | '/user/role' | '/role/$id/edit' + | '/user/change-password/$userName' + | '/user/role/$roleId' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -169,9 +232,15 @@ export interface FileRouteTypes { | '/device' | '/role' | '/rooms' + | '/profile/$userName' + | '/profile/change-password' | '/role/create' | '/rooms/$roomName' + | '/user/create' + | '/user/role' | '/role/$id/edit' + | '/user/change-password/$userName' + | '/user/role/$roleId' id: | '__root__' | '/' @@ -185,9 +254,15 @@ export interface FileRouteTypes { | '/_auth/device/' | '/_auth/role/' | '/_auth/rooms/' + | '/_auth/profile/$userName/' + | '/_auth/profile/change-password/' | '/_auth/role/create/' | '/_auth/rooms/$roomName/' + | '/_auth/user/create/' + | '/_auth/user/role/' | '/_auth/role/$id/edit/' + | '/_auth/user/change-password/$userName/' + | '/_auth/user/role/$roleId/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -275,6 +350,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof authLoginIndexRouteImport parentRoute: typeof rootRouteImport } + '/_auth/user/role/': { + id: '/_auth/user/role/' + path: '/user/role' + fullPath: '/user/role' + preLoaderRoute: typeof AuthUserRoleIndexRouteImport + parentRoute: typeof AuthRoute + } + '/_auth/user/create/': { + id: '/_auth/user/create/' + path: '/user/create' + fullPath: '/user/create' + preLoaderRoute: typeof AuthUserCreateIndexRouteImport + parentRoute: typeof AuthRoute + } '/_auth/rooms/$roomName/': { id: '/_auth/rooms/$roomName/' path: '/rooms/$roomName' @@ -289,6 +378,34 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthRoleCreateIndexRouteImport parentRoute: typeof AuthRoute } + '/_auth/profile/change-password/': { + id: '/_auth/profile/change-password/' + path: '/profile/change-password' + fullPath: '/profile/change-password' + preLoaderRoute: typeof AuthProfileChangePasswordIndexRouteImport + parentRoute: typeof AuthRoute + } + '/_auth/profile/$userName/': { + id: '/_auth/profile/$userName/' + path: '/profile/$userName' + fullPath: '/profile/$userName' + preLoaderRoute: typeof AuthProfileUserNameIndexRouteImport + parentRoute: typeof AuthRoute + } + '/_auth/user/role/$roleId/': { + id: '/_auth/user/role/$roleId/' + path: '/user/role/$roleId' + fullPath: '/user/role/$roleId' + preLoaderRoute: typeof AuthUserRoleRoleIdIndexRouteImport + parentRoute: typeof AuthRoute + } + '/_auth/user/change-password/$userName/': { + id: '/_auth/user/change-password/$userName/' + path: '/user/change-password/$userName' + fullPath: '/user/change-password/$userName' + preLoaderRoute: typeof AuthUserChangePasswordUserNameIndexRouteImport + parentRoute: typeof AuthRoute + } '/_auth/role/$id/edit/': { id: '/_auth/role/$id/edit/' path: '/role/$id/edit' @@ -308,9 +425,15 @@ interface AuthRouteChildren { AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute AuthRoleIndexRoute: typeof AuthRoleIndexRoute AuthRoomsIndexRoute: typeof AuthRoomsIndexRoute + AuthProfileUserNameIndexRoute: typeof AuthProfileUserNameIndexRoute + AuthProfileChangePasswordIndexRoute: typeof AuthProfileChangePasswordIndexRoute AuthRoleCreateIndexRoute: typeof AuthRoleCreateIndexRoute AuthRoomsRoomNameIndexRoute: typeof AuthRoomsRoomNameIndexRoute + AuthUserCreateIndexRoute: typeof AuthUserCreateIndexRoute + AuthUserRoleIndexRoute: typeof AuthUserRoleIndexRoute AuthRoleIdEditIndexRoute: typeof AuthRoleIdEditIndexRoute + AuthUserChangePasswordUserNameIndexRoute: typeof AuthUserChangePasswordUserNameIndexRoute + AuthUserRoleRoleIdIndexRoute: typeof AuthUserRoleRoleIdIndexRoute } const AuthRouteChildren: AuthRouteChildren = { @@ -322,9 +445,16 @@ const AuthRouteChildren: AuthRouteChildren = { AuthDeviceIndexRoute: AuthDeviceIndexRoute, AuthRoleIndexRoute: AuthRoleIndexRoute, AuthRoomsIndexRoute: AuthRoomsIndexRoute, + AuthProfileUserNameIndexRoute: AuthProfileUserNameIndexRoute, + AuthProfileChangePasswordIndexRoute: AuthProfileChangePasswordIndexRoute, AuthRoleCreateIndexRoute: AuthRoleCreateIndexRoute, AuthRoomsRoomNameIndexRoute: AuthRoomsRoomNameIndexRoute, + AuthUserCreateIndexRoute: AuthUserCreateIndexRoute, + AuthUserRoleIndexRoute: AuthUserRoleIndexRoute, AuthRoleIdEditIndexRoute: AuthRoleIdEditIndexRoute, + AuthUserChangePasswordUserNameIndexRoute: + AuthUserChangePasswordUserNameIndexRoute, + AuthUserRoleRoleIdIndexRoute: AuthUserRoleRoleIdIndexRoute, } const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) diff --git a/src/routes/_auth.tsx b/src/routes/_auth.tsx index e880cbc..13e1c85 100644 --- a/src/routes/_auth.tsx +++ b/src/routes/_auth.tsx @@ -45,6 +45,14 @@ function RouteComponent() { } }; + const handleProfile = () => { + navigate({ to: "/profile/$userName", params: { userName: auth.username } } as any); + }; + + const handleChangePassword = () => { + navigate({ to: "/profile/change-password" } as any); + }; + const username = auth.username; if (!auth.isAuthenticated) { @@ -61,7 +69,13 @@ function RouteComponent() {
- +
diff --git a/src/routes/_auth/profile/$userName/index.tsx b/src/routes/_auth/profile/$userName/index.tsx new file mode 100644 index 0000000..5875b57 --- /dev/null +++ b/src/routes/_auth/profile/$userName/index.tsx @@ -0,0 +1,89 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { User, Key, Shield } from "lucide-react"; +import { useAuth } from "@/hooks/useAuth"; + +export const Route = createFileRoute("/_auth/profile/$userName/")({ + component: UserProfileComponent, + loader: async ({ context, params }) => { + const { userName } = params as unknown as { userName: string }; + context.breadcrumbs = [ + { title: "Tài khoản", path: "#" }, + { title: "Thông tin cá nhân", path: `/profile/${userName}` }, + ]; + }, +}); + +function UserProfileComponent() { + const navigate = useNavigate(); + const auth = useAuth(); + const { userName } = Route.useParams() as { userName: string }; + + // Only allow viewing own profile + const isOwnProfile = auth.username === userName; + + if (!isOwnProfile) { + return ( +
+
+

Bạn không có quyền xem hồ sơ này

+ +
+
+ ); + } + + return ( +
+
+ {/* Avatar Section */} +
+
+ +
+
+ + {/* Info Section */} +
+
+ Tên đăng nhập + {auth.username} +
+ +
+ Họ và tên + {auth.name || "Chưa cập nhật"} +
+ +
+ Vai trò + + + {auth.role.roleName || "Chưa cập nhật"} + +
+ +
+ Cấp độ ưu tiên + {auth.role.priority} +
+
+ + {/* Action Button */} +
+ +
+
+
+ ); +} diff --git a/src/routes/_auth/profile/change-password/index.tsx b/src/routes/_auth/profile/change-password/index.tsx new file mode 100644 index 0000000..7ddedf8 --- /dev/null +++ b/src/routes/_auth/profile/change-password/index.tsx @@ -0,0 +1,157 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useChangePassword } from "@/hooks/queries"; +import { toast } from "sonner"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { LoaderCircle } from "lucide-react"; +import { useState } from "react"; + +export const Route = createFileRoute("/_auth/profile/change-password/")({ + component: SelfChangePasswordComponent, + loader: async ({ context }) => { + context.breadcrumbs = [ + { title: "Tài khoản", path: "#" }, + { title: "Đổi mật khẩu", path: "/profile/change-password" }, + ]; + }, +}); + +function SelfChangePasswordComponent() { + const navigate = useNavigate(); + const mutation = useChangePassword(); + + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(null); + + const validateForm = () => { + if (!currentPassword) { + setError("Mật khẩu hiện tại là bắt buộc"); + return false; + } + if (!newPassword) { + setError("Mật khẩu mới là bắt buộc"); + return false; + } + if (newPassword.length < 6) { + setError("Mật khẩu phải có ít nhất 6 ký tự"); + return false; + } + if (!confirmPassword) { + setError("Xác nhận mật khẩu là bắt buộc"); + return false; + } + if (newPassword !== confirmPassword) { + setError("Mật khẩu mới và xác nhận mật khẩu chưa giống nhau"); + return false; + } + if (currentPassword === newPassword) { + setError("Mật khẩu mới không được trùng với mật khẩu hiện tại"); + return false; + } + return true; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!validateForm()) return; + + mutation.mutate( + { currentPassword: currentPassword, newPassword: newPassword }, + { + onSuccess: () => { + toast.success("Đổi mật khẩu thành công"); + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + mutation.reset(); + }, + onError: () => { + toast.error("Đổi mật khẩu thất bại, có lỗi xảy ra vui lòng thử lại"); + }, + } + ); + }; + + const handleCancel = () => { + navigate({ to: "/dashboard" }); + }; + + return ( +
+
+
+
+ + setCurrentPassword(e.target.value)} + /> +
+ +
+ + setNewPassword(e.target.value)} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + /> +
+ + {error && ( + + Lỗi + {error} + + )} + + {mutation.isError && ( + + Lỗi + + Có lỗi xảy ra, vui lòng thử lại + + + )} + +
+ + +
+
+
+
+ ); +} diff --git a/src/routes/_auth/user/change-password/$userName/index.tsx b/src/routes/_auth/user/change-password/$userName/index.tsx new file mode 100644 index 0000000..fbb9e31 --- /dev/null +++ b/src/routes/_auth/user/change-password/$userName/index.tsx @@ -0,0 +1,140 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useChangePasswordAdmin } from "@/hooks/queries"; +import { toast } from "sonner"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { LoaderCircle } from "lucide-react"; +import { useState } from "react"; + +export const Route = createFileRoute("/_auth/user/change-password/$userName/")({ + component: AdminChangePasswordComponent, + loader: async ({ context, params }) => { + context.breadcrumbs = [ + { title: "Quản lý người dùng", path: "/user" }, + { + title: `Thay đổi mật khẩu của người dùng ${params.userName}`, + path: `/user/change-password/${params.userName}`, + }, + ]; + }, +}); + +function AdminChangePasswordComponent() { + const { userName } = Route.useParams(); + const navigate = useNavigate(); + const mutation = useChangePasswordAdmin(); + + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [error, setError] = useState(null); + + const validateForm = () => { + if (!newPassword) { + setError("Mật khẩu mới là bắt buộc"); + return false; + } + if (newPassword.length < 6) { + setError("Mật khẩu phải có ít nhất 6 ký tự"); + return false; + } + if (!confirmPassword) { + setError("Xác nhận mật khẩu là bắt buộc"); + return false; + } + if (newPassword !== confirmPassword) { + setError("Mật khẩu mới và xác nhận mật khẩu chưa giống nhau"); + return false; + } + return true; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!validateForm()) return; + + mutation.mutate( + { username: userName, newPassword: newPassword }, + { + onSuccess: () => { + toast.success("Cập nhật mật khẩu thành công"); + setNewPassword(""); + setConfirmPassword(""); + mutation.reset(); + }, + onError: () => { + toast.error("Cập nhật mật khẩu thất bại, có lỗi xảy ra vui lòng thử lại"); + }, + } + ); + }; + + const handleCancel = () => { + navigate({ to: "/dashboard" }); + }; + + return ( +
+
+
+
+ + setNewPassword(e.target.value)} + /> +
+ +
+ + setConfirmPassword(e.target.value)} + /> +
+ + {error && ( + + Lỗi + {error} + + )} + + {mutation.isError && ( + + Lỗi + + Có lỗi xảy ra, vui lòng thử lại + + + )} + +
+ + +
+
+
+
+ ); +} diff --git a/src/routes/_auth/user/create/index.tsx b/src/routes/_auth/user/create/index.tsx new file mode 100644 index 0000000..7cdf745 --- /dev/null +++ b/src/routes/_auth/user/create/index.tsx @@ -0,0 +1,312 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useGetRoleList, useCreateAccount } from "@/hooks/queries"; +import { useState } 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { UserPlus, ArrowLeft, Save, Loader2 } from "lucide-react"; +import { toast } from "sonner"; + +export const Route = createFileRoute("/_auth/user/create/")({ + component: CreateUserComponent, + loader: async ({ context }) => { + context.breadcrumbs = [ + { title: "Quản lý người dùng", path: "#" }, + { title: "Tạo người dùng mới", path: "/user/create" }, + ]; + }, +}); + +function CreateUserComponent() { + const navigate = useNavigate(); + const { data: roles = [], isLoading: rolesLoading } = useGetRoleList(); + const createMutation = useCreateAccount(); + + const [formData, setFormData] = useState({ + userName: "", + name: "", + password: "", + confirmPassword: "", + roleId: "", + }); + + const [errors, setErrors] = useState>({}); + + // Validate username theo regex backend + const validateUserName = (userName: string): boolean => { + const regex = /^[a-zA-Z0-9_.]{3,20}$/; + return regex.test(userName); + }; + + const validateForm = (): boolean => { + const newErrors: Record = {}; + + // Validate username + if (!formData.userName) { + newErrors.userName = "Tên đăng nhập không được để trống"; + } else if (!validateUserName(formData.userName)) { + newErrors.userName = "Tên đăng nhập chỉ cho phép chữ cái, số, dấu chấm và gạch dưới (3-20 ký tự)"; + } + + // Validate name + if (!formData.name.trim()) { + newErrors.name = "Họ và tên không được để trống"; + } + + // Validate password + if (!formData.password) { + newErrors.password = "Mật khẩu không được để trống"; + } else if (formData.password.length < 6) { + newErrors.password = "Mật khẩu phải có ít nhất 6 ký tự"; + } + + // Validate confirm password + if (formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = "Mật khẩu xác nhận không khớp"; + } + + // Validate roleId + if (!formData.roleId) { + newErrors.roleId = "Vui lòng chọn vai trò"; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) { + return; + } + + try { + await createMutation.mutateAsync({ + userName: formData.userName, + name: formData.name, + password: formData.password, + roleId: Number(formData.roleId), + accessRooms: [0], // Default value, will be updated when Room API provides IDs + }); + toast.success("Tạo tài khoản thành công!"); + navigate({ to: "/dashboard" }); // TODO: Navigate to user list page when it exists + } catch (error: any) { + const errorMessage = error.response?.data?.message || "Tạo tài khoản thất bại!"; + toast.error(errorMessage); + } + }; + + const handleInputChange = (field: string, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + // Clear error when user starts typing + if (errors[field]) { + setErrors((prev) => { + const newErrors = { ...prev }; + delete newErrors[field]; + return newErrors; + }); + } + }; + + return ( +
+ {/* Header */} +
+
+

Tạo người dùng mới

+

+ Thêm tài khoản người dùng mới vào hệ thống +

+
+ +
+ + {/* Form */} +
+ + + + + Thông tin tài khoản + + + Điền thông tin để tạo tài khoản người dùng mới + + + + {/* Username and Name - Grid Layout */} +
+
+ + handleInputChange("userName", e.target.value)} + placeholder="Nhập tên đăng nhập (3-20 ký tự, chỉ chữ, số, . và _)" + disabled={createMutation.isPending} + className="h-10" + /> + {errors.userName && ( +

{errors.userName}

+ )} +
+ +
+ + handleInputChange("name", e.target.value)} + placeholder="Nhập họ và tên đầy đủ" + disabled={createMutation.isPending} + className="h-10" + /> + {errors.name && ( +

{errors.name}

+ )} +
+
+ + {/* Password */} +
+
+ + handleInputChange("password", e.target.value)} + placeholder="Nhập mật khẩu (tối thiểu 6 ký tự)" + disabled={createMutation.isPending} + className="h-10" + /> + {errors.password && ( +

{errors.password}

+ )} +
+ +
+ + handleInputChange("confirmPassword", e.target.value)} + placeholder="Nhập lại mật khẩu" + disabled={createMutation.isPending} + className="h-10" + /> + {errors.confirmPassword && ( +

{errors.confirmPassword}

+ )} +
+
+ + {/* Role Selection */} +
+ + {rolesLoading ? ( +
+ +
+ ) : ( + + )} + {errors.roleId && ( +

{errors.roleId}

+ )} +
+ + {/* TODO: Add Room/Building Access selection when API is ready */} + {/* +
+ +

+ Chọn các phòng mà người dùng có quyền truy cập +

+ // Add multi-select component here when Room API provides IDs +
+ */} +
+
+ + {/* Actions */} +
+ + +
+
+
+ ); +} diff --git a/src/routes/_auth/user/role/$roleId/index.tsx b/src/routes/_auth/user/role/$roleId/index.tsx new file mode 100644 index 0000000..0e2e0a1 --- /dev/null +++ b/src/routes/_auth/user/role/$roleId/index.tsx @@ -0,0 +1,132 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useGetRoleById, useGetRolePermissions } from "@/hooks/queries"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Shield, ArrowLeft, Check, X } from "lucide-react"; +import type { PermissionOnRole } from "@/types/permission"; + +export const Route = createFileRoute("/_auth/user/role/$roleId/")({ + component: ViewRolePermissionsComponent, + loader: async ({ context, params }) => { + context.breadcrumbs = [ + { title: "Quản lý người dùng", path: "#" }, + { title: `Quyền của Role #${params.roleId}`, path: `/user/role/${params.roleId}` }, + ]; + }, +}); + +function ViewRolePermissionsComponent() { + const { roleId } = Route.useParams(); + const navigate = useNavigate(); + const roleIdNum = parseInt(roleId, 10); + + const { data: role, isLoading: roleLoading } = useGetRoleById(roleIdNum); + const { data: permissions = [], isLoading: permissionsLoading } = useGetRolePermissions(roleIdNum); + + const isLoading = roleLoading || permissionsLoading; + + // Group permissions by parent + const groupedPermissions = (permissions as PermissionOnRole[]).reduce((acc, permission) => { + if (permission.parentId === null) { + if (!acc[permission.permisionId]) { + acc[permission.permisionId] = { parent: permission, children: [] }; + } else { + acc[permission.permisionId].parent = permission; + } + } else { + if (!acc[permission.parentId]) { + acc[permission.parentId] = { parent: null as any, children: [] }; + } + acc[permission.parentId].children.push(permission); + } + return acc; + }, {} as Record); + + if (isLoading) { + return ( +
+
+
+ ); + } + + return ( +
+
+ +
+ + + + + + Quyền hạn của Role: {role?.roleName || `#${roleId}`} + + + Danh sách các quyền được gán cho role này + {role?.priority !== undefined && ( + (Độ ưu tiên: {role.priority}) + )} + + + + {permissions.length === 0 ? ( +
Không có quyền nào được gán cho role này
+ ) : ( +
+ {Object.values(groupedPermissions).map(({ parent, children }) => ( +
+
+
+ {parent?.permissionName || "Unknown"} + {parent?.permissionCode} +
+
+ {parent?.isChecked === 1 ? ( + + Đã bật + + ) : ( + + Đã tắt + + )} +
+
+ + {children.length > 0 && ( +
+ {children.map((child) => ( +
+
+ {child.permissionName} + {child.permissionCode} +
+ {child.isChecked === 1 ? ( + + ) : ( + + )} +
+ ))} +
+ )} +
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/routes/_auth/user/role/index.tsx b/src/routes/_auth/user/role/index.tsx new file mode 100644 index 0000000..f720d09 --- /dev/null +++ b/src/routes/_auth/user/role/index.tsx @@ -0,0 +1,133 @@ +import { createFileRoute, useNavigate } from "@tanstack/react-router"; +import { useGetRoleList } from "@/hooks/queries"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Shield, Eye, ArrowLeft, Loader2 } from "lucide-react"; + +export const Route = createFileRoute("/_auth/user/role/")({ + component: RoleListComponent, + loader: async ({ context }) => { + context.breadcrumbs = [ + { title: "Quản lý người dùng", path: "#" }, + { title: "Danh sách vai trò", path: "/user/role" }, + ]; + }, +}); + +function RoleListComponent() { + const navigate = useNavigate(); + const { data: roles, isLoading, isError } = useGetRoleList(); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( +
+ + +
+ Không thể tải danh sách vai trò +
+
+ +
+
+
+
+ ); + } + + return ( +
+
+ +
+ + + + + + Danh sách vai trò + + + Quản lý các vai trò và quyền hạn trong hệ thống + + + + {roles && roles.length > 0 ? ( + + + + ID + Tên vai trò + Độ ưu tiên + Ngày tạo + Thao tác + + + + {roles.map((role) => ( + + + {role.id} + + {role.roleName} + + {role.priority} + + + {role.createdAt ? new Date(role.createdAt).toLocaleDateString("vi-VN") : "—"} + + + + + + ))} + +
+ ) : ( +
+ Không có vai trò nào trong hệ thống +
+ )} +
+
+
+ ); +} diff --git a/src/services/app-version.service.ts b/src/services/app-version.service.ts index 04f5aa0..5677c32 100644 --- a/src/services/app-version.service.ts +++ b/src/services/app-version.service.ts @@ -1,4 +1,4 @@ -import axios, { type AxiosProgressEvent } from "axios"; +import axios, { type AxiosProgressEvent } from "@/config/axios"; import { API_ENDPOINTS } from "@/config/api"; import type { Version } from "@/types/file"; diff --git a/src/services/auth.service.ts b/src/services/auth.service.ts index ba55917..c7d5dde 100644 --- a/src/services/auth.service.ts +++ b/src/services/auth.service.ts @@ -1,6 +1,6 @@ -import axios from "axios"; +import axios from "@/config/axios"; import { API_ENDPOINTS } from "@/config/api"; -import type { LoginResquest, LoginResponse } from "@/types/auth"; +import type { LoginResquest, LoginResponse, CreateAccountRequest } from "@/types/auth"; /** * Đăng nhập @@ -72,15 +72,9 @@ export async function changePasswordAdmin(data: { /** * Tạo tài khoản mới - * @param data - Dữ liệu tạo tài khoản + * @param data - Dữ liệu tạo tài khoản (maps to CreateAccountRequest DTO) */ -export async function createAccount(data: { - userName: string; - password: string; - name: string; - roleId: number; - accessBuildings?: number[]; -}): Promise<{ message: string }> { +export async function createAccount(data: CreateAccountRequest): Promise<{ message: string }> { const response = await axios.post(API_ENDPOINTS.AUTH.CREATE_ACCOUNT, data); return response.data; } diff --git a/src/services/command.service.ts b/src/services/command.service.ts index bfe839c..325641b 100644 --- a/src/services/command.service.ts +++ b/src/services/command.service.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios from "@/config/axios"; import { API_ENDPOINTS } from "@/config/api"; /** diff --git a/src/services/device-comm.service.ts b/src/services/device-comm.service.ts index 2092069..f65bd18 100644 --- a/src/services/device-comm.service.ts +++ b/src/services/device-comm.service.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios from "@/config/axios"; import { API_ENDPOINTS } from "@/config/api"; import type { DeviceHealthCheck } from "@/types/device"; diff --git a/src/services/permission.service.ts b/src/services/permission.service.ts index d160d48..19a5856 100644 --- a/src/services/permission.service.ts +++ b/src/services/permission.service.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios from "@/config/axios"; import { API_ENDPOINTS } from "@/config/api"; import type { Permission } from "@/types/permission"; diff --git a/src/services/role.service.ts b/src/services/role.service.ts index 83eda89..44648fb 100644 --- a/src/services/role.service.ts +++ b/src/services/role.service.ts @@ -1,4 +1,4 @@ -import axios from "axios"; +import axios from "@/config/axios"; import { API_ENDPOINTS } from "@/config/api"; import type { TCreateRoleRequestBody, TRoleResponse } from "@/types/role"; import type { PermissionOnRole } from "@/types/permission"; diff --git a/src/template/command-submit-template.tsx b/src/template/command-submit-template.tsx index 9ee46d7..9d46e68 100644 --- a/src/template/command-submit-template.tsx +++ b/src/template/command-submit-template.tsx @@ -200,14 +200,16 @@ export function CommandSubmitTemplate({ }; return ( -
-
-

{title}

-

{description}

+
+
+
+

{title}

+

{description}

+
- - + + {title} @@ -221,9 +223,9 @@ export function CommandSubmitTemplate({ )} - + {/* Tabs Navigation */} -
+
- {/* Tab 1: Danh sách */} {activeTab === "list" && ( -
-
- - data={data} - columns={columns} - isLoading={isLoading} - onTableInit={handleTableInit} - onRowClick={onRowClick} - scrollable={scrollable} - maxHeight={maxHeight} - enablePagination={enablePagination} - defaultPageSize={defaultPageSize} - pageSizeOptions={pageSizeOptions} - /> -
-
+
+ + data={data} + columns={columns} + isLoading={isLoading} + onTableInit={handleTableInit} + onRowClick={onRowClick} + scrollable={!enablePagination && scrollable} + maxHeight={maxHeight} + enablePagination={enablePagination} + defaultPageSize={defaultPageSize} + pageSizeOptions={pageSizeOptions} + /> +
({ {/* Tab 2: Thực thi */} {activeTab === "execute" && ( -
+
{/* Lệnh tùy chỉnh */} - + Thực Thi Lệnh Tùy Chỉnh diff --git a/src/types/app-sidebar.ts b/src/types/app-sidebar.ts index 5ecf292..027da65 100644 --- a/src/types/app-sidebar.ts +++ b/src/types/app-sidebar.ts @@ -1,4 +1,4 @@ -import { AppWindow, Building, CircleX, Home, ShieldCheck, Terminal} from "lucide-react"; +import { AppWindow, Building, CircleX, Home, ShieldCheck, Terminal, UserPlus} from "lucide-react"; import { PermissionEnum } from "./permission"; enum AppSidebarSectionCode { @@ -85,6 +85,12 @@ export const appSidebarSection = { url: "/role", icon: ShieldCheck, permissions: [PermissionEnum.VIEW_ROLES], + }, + { + title: "Tạo người dùng", + url: "/user/create", + icon: UserPlus, + permissions: [PermissionEnum.CRE_USER], } ] } diff --git a/src/types/auth.ts b/src/types/auth.ts index a059cb3..682a5a0 100644 --- a/src/types/auth.ts +++ b/src/types/auth.ts @@ -5,6 +5,7 @@ export interface IAuthContext { token: string; name: string; acs: number[]; + isSystemAdmin?: () => boolean; role: { roleName: string; priority: number; @@ -28,3 +29,15 @@ export type LoginResponse = { priority: number; }; }; + +/** + * Request DTO for creating new account + * Maps to backend CreateAccountRequest DTO + */ +export type CreateAccountRequest = { + name: string; + userName: string; + password: string; + roleId: number; + accessRooms?: number[] | null; +};