@@ -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
+
navigate({ to: "/dashboard" })}>
+ Quay lại
+
+
+
+ );
+ }
+
+ 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 */}
+
+ navigate({ to: "/profile/change-password" as any })}
+ >
+
+ Đổi mật khẩu
+
+
+
+
+ );
+}
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 (
+
+ );
+}
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 (
+
+ );
+}
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
+
+
+
navigate({ to: "/dashboard" })}
+ >
+
+ Quay lại
+
+
+
+ {/* Form */}
+
+
+ );
+}
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 (
+
+
+
navigate({ to: "/dashboard" })}>
+
+ Quay lại
+
+
+
+
+
+
+
+ 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ò
+
+
+
navigate({ to: "/dashboard" })}>
+
+ Quay lại
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
navigate({ to: "/dashboard" })}>
+
+ Quay lại
+
+
+
+
+
+
+
+ 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") : "—"}
+
+
+
+ navigate({ to: "/user/role/$roleId", params: { roleId: String(role.id) } } as any)
+ }
+ >
+
+ Xem quyền
+
+
+
+ ))}
+
+
+ ) : (
+
+ 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 */}
-
+
setActiveTab("list")}
className={`pb-3 px-2 font-medium text-sm flex items-center gap-2 transition-colors ${
@@ -247,25 +249,22 @@ export function CommandSubmitTemplate({
Lệnh thủ công
-
{/* 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;
+};