diff --git a/HOOKS_USAGE_GUIDE.md b/HOOKS_USAGE_GUIDE.md
new file mode 100644
index 0000000..59efbdd
--- /dev/null
+++ b/HOOKS_USAGE_GUIDE.md
@@ -0,0 +1,341 @@
+# TanStack Query Hooks Documentation
+
+Tất cả các API đã được tách riêng thành TanStack Query hooks trong folder `src/hooks/queries/`.
+
+## Cấu trúc
+
+```
+src/hooks/queries/
+ ├── index.ts # Export tất cả hooks
+ ├── useAuthQueries.ts # Auth hooks
+ ├── useAppVersionQueries.ts # App/Software hooks
+ ├── useDeviceCommQueries.ts # Device communication hooks
+ └── useCommandQueries.ts # Command hooks
+```
+
+## Cách Sử Dụng
+
+### 1. Auth Queries (Xác thực)
+
+#### Đăng nhập
+```tsx
+import { useLogin } from '@/hooks/queries'
+
+function LoginPage() {
+ const loginMutation = useLogin()
+
+ const handleLogin = async () => {
+ try {
+ await loginMutation.mutateAsync({
+ username: 'user',
+ password: 'password'
+ })
+ // Tự động lưu token vào localStorage
+ } catch (error) {
+ console.error(error)
+ }
+ }
+
+ return (
+
+ )
+}
+```
+
+#### Đăng xuất
+```tsx
+import { useLogout } from '@/hooks/queries'
+
+function LogoutButton() {
+ const logoutMutation = useLogout()
+
+ return (
+
+ )
+}
+```
+
+#### Kiểm tra phiên
+```tsx
+import { usePing } from '@/hooks/queries'
+
+function CheckSession() {
+ const { data, isLoading } = usePing(token, true)
+
+ if (isLoading) return
Checking...
+ return Session: {data?.message}
+}
+```
+
+#### Thay đổi mật khẩu
+```tsx
+import { useChangePassword } from '@/hooks/queries'
+
+function ChangePasswordForm() {
+ const changePasswordMutation = useChangePassword()
+
+ const handleSubmit = async () => {
+ await changePasswordMutation.mutateAsync({
+ currentPassword: 'old',
+ newPassword: 'new'
+ })
+ }
+
+ return
+}
+```
+
+### 2. App Version Queries (Phần mềm/Agent)
+
+#### Lấy danh sách agent
+```tsx
+import { useGetAgentVersion } from '@/hooks/queries'
+
+function AgentList() {
+ const { data: agents, isLoading } = useGetAgentVersion()
+
+ if (isLoading) return Loading...
+ return {agents?.length} agents
+}
+```
+
+#### Lấy danh sách phần mềm
+```tsx
+import { useGetSoftwareList } from '@/hooks/queries'
+
+function SoftwareList() {
+ const { data: software, isLoading } = useGetSoftwareList()
+
+ return software?.map(item => {item.name}
)
+}
+```
+
+#### Upload file
+```tsx
+import { useUploadSoftware } from '@/hooks/queries'
+
+function UploadForm() {
+ const uploadMutation = useUploadSoftware()
+
+ const handleUpload = async (file: File) => {
+ const formData = new FormData()
+ formData.append('file', file)
+
+ await uploadMutation.mutateAsync({
+ formData,
+ onUploadProgress: (event) => {
+ const percent = (event.loaded / event.total) * 100
+ console.log(`Upload: ${percent}%`)
+ }
+ })
+ }
+
+ return e.target.files && handleUpload(e.target.files[0])} />
+}
+```
+
+#### Quản lý blacklist
+```tsx
+import {
+ useGetBlacklist,
+ useAddBlacklist,
+ useDeleteBlacklist
+} from '@/hooks/queries'
+
+function BlacklistManager() {
+ const { data: blacklist } = useGetBlacklist()
+ const addMutation = useAddBlacklist()
+ const deleteMutation = useDeleteBlacklist()
+
+ const handleAdd = async () => {
+ await addMutation.mutateAsync({ appId: 1 })
+ }
+
+ const handleDelete = async (appId: number) => {
+ await deleteMutation.mutateAsync(appId)
+ }
+
+ return (
+ <>
+ {blacklist?.map(item => (
+
+ {item.name}
+
+
+ ))}
+
+ >
+ )
+}
+```
+
+### 3. Device Communication Queries
+
+#### Lấy danh sách phòng
+```tsx
+import { useGetRoomList } from '@/hooks/queries'
+
+function RoomSelector() {
+ const { data: rooms } = useGetRoomList()
+
+ return (
+
+ )
+}
+```
+
+#### Lấy thiết bị trong phòng
+```tsx
+import { useGetDeviceFromRoom } from '@/hooks/queries'
+
+function DeviceList({ roomName }: { roomName: string }) {
+ const { data: devices, isLoading } = useGetDeviceFromRoom(roomName, true)
+
+ if (isLoading) return Loading devices...
+
+ return devices?.map(device => (
+ {device.name}
+ ))
+}
+```
+
+#### Gửi lệnh
+```tsx
+import { useSendCommand } from '@/hooks/queries'
+
+function CommandForm() {
+ const sendMutation = useSendCommand()
+
+ const handleSend = async () => {
+ await sendMutation.mutateAsync({
+ roomName: 'Room A',
+ data: { command: 'dir' }
+ })
+ }
+
+ return
+}
+```
+
+#### Cài đặt phần mềm
+```tsx
+import { useInstallMsi } from '@/hooks/queries'
+
+function InstallSoftware() {
+ const installMutation = useInstallMsi()
+
+ const handleInstall = async () => {
+ await installMutation.mutateAsync({
+ roomName: 'Room A',
+ data: { msiFileId: 1 }
+ })
+ }
+
+ return
+}
+```
+
+### 4. Command Queries
+
+#### Lấy danh sách lệnh
+```tsx
+import { useGetCommandList } from '@/hooks/queries'
+
+function CommandList() {
+ const { data: commands } = useGetCommandList()
+
+ return commands?.map(cmd => {cmd.name}
)
+}
+```
+
+#### Thêm lệnh
+```tsx
+import { useAddCommand } from '@/hooks/queries'
+
+function AddCommandForm() {
+ const addMutation = useAddCommand()
+
+ const handleAdd = async () => {
+ await addMutation.mutateAsync({
+ name: 'My Command',
+ command: 'echo hello'
+ })
+ }
+
+ return
+}
+```
+
+## Lợi ích
+
+1. **Automatic Caching** - TanStack Query tự động cache dữ liệu
+2. **Background Refetching** - Cập nhật dữ liệu trong background
+3. **Stale Time Management** - Kiểm soát thời gian dữ liệu còn "fresh"
+4. **Automatic Invalidation** - Tự động update dữ liệu sau mutations
+5. **Deduplication** - Gộp các request giống nhau
+6. **Error Handling** - Xử lý lỗi tập trung
+7. **Loading States** - Tracking loading/pending/error states
+
+## Advanced Usage
+
+### Dependent Queries
+```tsx
+function DeviceDetails({ deviceId }: { deviceId: number }) {
+ const { data: device } = useGetDeviceFromRoom(deviceId, true)
+
+ // Chỉ fetch khi có device
+ const { data: status } = useGetClientFolderStatus(
+ device?.roomName,
+ !!device
+ )
+
+ return {status?.path}
+}
+```
+
+### Prefetching
+```tsx
+import { useQueryClient } from '@tanstack/react-query'
+import { useGetSoftwareList } from '@/hooks/queries'
+
+function PrefetchOnHover() {
+ const queryClient = useQueryClient()
+
+ const handleMouseEnter = () => {
+ queryClient.prefetchQuery({
+ queryKey: ['app-version', 'software'],
+ queryFn: () => useGetSoftwareList
+ })
+ }
+
+ return Hover me
+}
+```
+
+## Migration từ cách cũ
+
+**Trước:**
+```tsx
+const { data } = useQueryData({
+ queryKey: ["software-version"],
+ url: API_ENDPOINTS.APP_VERSION.GET_SOFTWARE,
+})
+```
+
+**Sau:**
+```tsx
+const { data } = useGetSoftwareList()
+```
+
+Đơn giản hơn, type-safe hơn, và dễ bảo trì hơn!
diff --git a/SERVICES_GUIDE.md b/SERVICES_GUIDE.md
new file mode 100644
index 0000000..5b41a17
--- /dev/null
+++ b/SERVICES_GUIDE.md
@@ -0,0 +1,174 @@
+# API Services Documentation
+
+Tất cả logic gọi API đã được tách riêng vào folder `services`. Mỗi service tương ứng với một nhóm API.
+
+## Cấu trúc Services
+
+```
+src/services/
+ ├── index.ts # Export tất cả services
+ ├── auth.service.ts # API xác thực
+ ├── app-version.service.ts # API quản lý phần mềm
+ ├── device-comm.service.ts # API thiết bị
+ ├── command.service.ts # API lệnh
+ └── device.service.ts # Helper functions
+```
+
+## Cách Sử Dụng
+
+### 1. Auth Service (Xác thực)
+
+```tsx
+import { authService } from '@/services'
+
+// Đăng nhập
+const response = await authService.login({
+ username: 'user',
+ password: 'pass'
+})
+
+// Đăng xuất
+await authService.logout()
+
+// Kiểm tra session
+const pingResult = await authService.ping(token)
+
+// Thay đổi mật khẩu
+await authService.changePassword({
+ currentPassword: 'old',
+ newPassword: 'new'
+})
+
+// Tạo tài khoản mới (admin)
+await authService.createAccount({
+ userName: 'newuser',
+ password: 'pass',
+ name: 'John Doe',
+ roleId: 1,
+ accessBuildings: [1, 2, 3]
+})
+```
+
+### 2. App Version Service (Quản lý phần mềm)
+
+```tsx
+import { appVersionService } from '@/services'
+
+// Lấy danh sách agent
+const agents = await appVersionService.getAgentVersion()
+
+// Lấy danh sách phần mềm
+const software = await appVersionService.getSoftwareList()
+
+// Upload file
+const formData = new FormData()
+formData.append('file', fileInput.files[0])
+await appVersionService.uploadSoftware(formData, (progressEvent) => {
+ console.log(`Progress: ${progressEvent.loaded}/${progressEvent.total}`)
+})
+
+// Lấy blacklist
+const blacklist = await appVersionService.getBlacklist()
+
+// Thêm vào blacklist
+await appVersionService.addBlacklist({ appId: 1, reason: 'virus' })
+
+// Xóa khỏi blacklist
+await appVersionService.deleteBlacklist(1)
+```
+
+### 3. Device Comm Service (Thiết bị)
+
+```tsx
+import { deviceCommService } from '@/services'
+
+// Lấy tất cả thiết bị
+const allDevices = await deviceCommService.getAllDevices()
+
+// Lấy danh sách phòng
+const rooms = await deviceCommService.getRoomList()
+
+// Lấy thiết bị trong phòng
+const devices = await deviceCommService.getDeviceFromRoom('Room A')
+
+// Gửi lệnh
+await deviceCommService.sendCommand('Room A', {
+ command: 'dir'
+})
+
+// Cập nhật agent
+await deviceCommService.updateAgent('Room A', { version: '1.0.0' })
+
+// Cài đặt MSI
+await deviceCommService.installMsi('Room A', { msiFileId: 1 })
+```
+
+### 4. Command Service (Lệnh)
+
+```tsx
+import { commandService } from '@/services'
+
+// Lấy danh sách lệnh
+const commands = await commandService.getCommandList()
+
+// Thêm lệnh
+await commandService.addCommand({ name: 'cmd1', command: 'echo hello' })
+
+// Cập nhật lệnh
+await commandService.updateCommand(1, { name: 'cmd1 updated' })
+
+// Xóa lệnh
+await commandService.deleteCommand(1)
+```
+
+## Sử dụng với React Query/Hooks
+
+### Cách cũ (trực tiếp gọi từ component):
+```tsx
+const { data } = useQueryData({
+ queryKey: ["software-version"],
+ url: API_ENDPOINTS.APP_VERSION.GET_SOFTWARE,
+})
+```
+
+### Cách mới (tách biệt logic):
+
+Có thể tạo custom hooks bao quanh services:
+
+```tsx
+// hooks/useGetSoftware.ts
+import { useQuery } from '@tanstack/react-query'
+import { appVersionService } from '@/services'
+
+export function useGetSoftware() {
+ return useQuery({
+ queryKey: ['software-version'],
+ queryFn: () => appVersionService.getSoftwareList(),
+ staleTime: 60 * 1000,
+ })
+}
+```
+
+Sau đó sử dụng trong component:
+```tsx
+function AppsComponent() {
+ const { data, isLoading } = useGetSoftware()
+ // ...
+}
+```
+
+## Lợi ích của cách sử dụng mới
+
+1. **Tách biệt logic** - Logic API nằm riêng, dễ bảo trì
+2. **Tái sử dụng** - Có thể sử dụng service từ bất kỳ nơi
+3. **Dễ test** - Dễ mock services khi viết unit tests
+4. **Centralized error handling** - Có thể xử lý lỗi chung
+5. **Type safety** - TypeScript types cho tất cả API requests/responses
+
+## Cải tiến trong tương lai
+
+Có thể thêm:
+- Global error handling middleware trong axios
+- Request/response interceptors cho authentication
+- Retry logic cho failed requests
+- Request cancellation
diff --git a/SERVICES_VS_HOOKS.md b/SERVICES_VS_HOOKS.md
new file mode 100644
index 0000000..b5b03cf
--- /dev/null
+++ b/SERVICES_VS_HOOKS.md
@@ -0,0 +1,328 @@
+# Khác biệt giữa Services và Query Hooks
+
+## Tóm tắt nhanh
+
+| Aspect | Services | Query Hooks |
+|--------|----------|-------------|
+| **Location** | `src/services/` | `src/hooks/queries/` |
+| **Mục đích** | Gọi API trực tiếp | Wrapper TanStack Query |
+| **Caching** | ❌ Không | ✅ Có |
+| **Background Refetch** | ❌ Không | ✅ Có |
+| **Auto Invalidation** | ❌ Không | ✅ Có |
+| **Type** | Async functions | React Hooks |
+| **Dùng trong** | Non-React code, utilities | React components |
+
+---
+
+## Chi tiết Từng Layer
+
+### 1. Services Layer (`src/services/`)
+
+**Mục đích:** Đơn thuần gọi API và trả về dữ liệu
+
+```typescript
+// app-version.service.ts
+export async function getSoftwareList(): Promise {
+ const response = await axios.get(
+ API_ENDPOINTS.APP_VERSION.GET_SOFTWARE
+ );
+ return response.data;
+}
+```
+
+**Đặc điểm:**
+- ✅ Pure async functions
+- ✅ Không phụ thuộc vào React
+- ✅ Có thể sử dụng ở bất kỳ đâu (utils, servers, non-React code)
+- ❌ Không có caching
+- ❌ Phải tự quản lý state loading/error
+- ❌ Phải tự gọi lại khi dữ liệu thay đổi
+
+**Khi nào dùng:**
+```typescript
+// Dùng trong utility functions
+export async function initializeApp() {
+ const software = await appVersionService.getSoftwareList();
+ // ...
+}
+
+// Hoặc trong services khác
+export async function validateSoftware() {
+ const list = await appVersionService.getSoftwareList();
+ // ...
+}
+```
+
+---
+
+### 2. Query Hooks Layer (`src/hooks/queries/`)
+
+**Mục đích:** Wrapper TanStack Query bên trên services
+
+```typescript
+// useAppVersionQueries.ts
+export function useGetSoftwareList(enabled = true) {
+ return useQuery({
+ queryKey: ["app-version", "software"],
+ queryFn: () => appVersionService.getSoftwareList(),
+ enabled,
+ staleTime: 60 * 1000, // 1 minute
+ });
+}
+```
+
+**Đặc điểm:**
+- ✅ React hooks
+- ✅ Automatic caching
+- ✅ Background refetching
+- ✅ Automatic invalidation sau mutations
+- ✅ Built-in loading/error states
+- ✅ Deduplication (gộp requests giống nhau)
+- ❌ Chỉ dùng được trong React components
+- ❌ Phức tạp hơn services
+
+**Khi nào dùng:**
+```typescript
+// Dùng trong React components
+function SoftwareList() {
+ const { data: software, isLoading } = useGetSoftwareList()
+
+ if (isLoading) return Loading...
+ return software?.map(item => {item.name}
)
+}
+```
+
+---
+
+## So sánh cụ thể
+
+### Ví dụ 1: Lấy danh sách
+
+**Service - Raw API call:**
+```typescript
+// services/app-version.service.ts
+export async function getSoftwareList(): Promise {
+ const response = await axios.get(API_ENDPOINTS.APP_VERSION.GET_SOFTWARE);
+ return response.data;
+}
+```
+
+**Hook - TanStack Query wrapper:**
+```typescript
+// hooks/queries/useAppVersionQueries.ts
+export function useGetSoftwareList(enabled = true) {
+ return useQuery({
+ queryKey: ["app-version", "software"],
+ queryFn: () => appVersionService.getSoftwareList(),
+ staleTime: 60 * 1000,
+ });
+}
+```
+
+**Sử dụng trong component:**
+```typescript
+function Component() {
+ // ❌ KHÔNG nên dùng service trực tiếp
+ const [data, setData] = useState([]);
+ const [loading, setLoading] = useState(true);
+ useEffect(() => {
+ appVersionService.getSoftwareList().then(d => {
+ setData(d);
+ setLoading(false);
+ });
+ }, []);
+
+ // ✅ NÊN dùng hook thay vì
+ const { data, isLoading } = useGetSoftwareList();
+}
+```
+
+---
+
+### Ví dụ 2: Upload file
+
+**Service:**
+```typescript
+// services/app-version.service.ts
+export async function uploadSoftware(
+ formData: FormData,
+ onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
+): Promise<{ message: string }> {
+ return axios.post(API_ENDPOINTS.APP_VERSION.UPLOAD, formData, {
+ onUploadProgress,
+ });
+}
+```
+
+**Hook:**
+```typescript
+// hooks/queries/useAppVersionQueries.ts
+export function useUploadSoftware() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data: { formData: FormData; onUploadProgress?: ... }) =>
+ appVersionService.uploadSoftware(data.formData, data.onUploadProgress),
+ onSuccess: () => {
+ // Tự động invalidate software list
+ queryClient.invalidateQueries({
+ queryKey: ["app-version", "software"],
+ });
+ },
+ });
+}
+```
+
+**Sử dụng:**
+```typescript
+function UploadForm() {
+ const uploadMutation = useUploadSoftware();
+
+ const handleUpload = async (file: File) => {
+ const formData = new FormData();
+ formData.append("file", file);
+
+ await uploadMutation.mutateAsync({
+ formData,
+ onUploadProgress: (e) => console.log(`${e.loaded}/${e.total}`)
+ });
+
+ // ✅ Software list tự động update
+ };
+
+ return (
+
+ );
+}
+```
+
+---
+
+## Architecture Flow
+
+```
+┌─────────────────────────────────────────┐
+│ React Component │
+│ (SoftwareList, UploadForm, etc.) │
+└──────────────┬──────────────────────────┘
+ │
+ │ uses
+ ▼
+┌─────────────────────────────────────────┐
+│ Query Hooks (src/hooks/queries/) │
+│ - useGetSoftwareList() │
+│ - useUploadSoftware() │
+│ - useDeleteBlacklist() │
+│ - Features: │
+│ - Caching │
+│ - Auto invalidation │
+│ - Loading states │
+└──────────────┬──────────────────────────┘
+ │
+ │ wraps
+ ▼
+┌─────────────────────────────────────────┐
+│ Service Functions (src/services/) │
+│ - getSoftwareList() │
+│ - uploadSoftware() │
+│ - deleteBlacklist() │
+│ - Features: │
+│ - Pure async functions │
+│ - Direct API calls │
+└──────────────┬──────────────────────────┘
+ │
+ │ uses
+ ▼
+┌─────────────────────────────────────────┐
+│ Axios (HTTP Client) │
+└──────────────┬──────────────────────────┘
+ │
+ │ requests
+ ▼
+┌─────────────────────────────────────────┐
+│ Backend API Server │
+└─────────────────────────────────────────┘
+```
+
+---
+
+## Nguyên tắc sử dụng
+
+### ✅ NÊN dùng Services khi:
+- Gọi API từ non-React code (utilities, event handlers, etc.)
+- Cần gọi API một lần rồi không cần tracking state
+- Không cần caching hay background refetch
+- Viết code không phụ thuộc React
+
+```typescript
+// ✅ OK - utility function
+export async function syncData() {
+ const software = await appVersionService.getSoftwareList();
+ const commands = await commandService.getCommandList();
+ return { software, commands };
+}
+```
+
+### ✅ NÊN dùng Hooks khi:
+- Lấy/update dữ liệu trong React components
+- Cần caching và background refetch
+- Muốn dữ liệu tự động update
+- Cần tracking loading/error states
+
+```typescript
+// ✅ OK - React component
+function Dashboard() {
+ const { data: software, isLoading } = useGetSoftwareList();
+ const uploadMutation = useUploadSoftware();
+
+ return {/* ... */}
;
+}
+```
+
+### ❌ KHÔNG nên dùng Services khi:
+- Đang trong React component và cần state management
+- Cần automatic refetching
+- Cần auto-invalidation sau mutations
+
+```typescript
+// ❌ WRONG
+function Component() {
+ const [data, setData] = useState([]);
+ useEffect(() => {
+ appVersionService.getSoftwareList().then(setData);
+ }, []);
+}
+
+// ✅ RIGHT
+function Component() {
+ const { data } = useGetSoftwareList();
+}
+```
+
+### ❌ KHÔNG nên dùng Hooks khi:
+- Không phải trong React component
+- Không có React context
+
+```typescript
+// ❌ WRONG - không thể gọi hooks ở đây
+export function initApp() {
+ const { data } = useGetSoftwareList(); // ERROR!
+}
+
+// ✅ RIGHT
+export async function initApp() {
+ const data = await appVersionService.getSoftwareList();
+}
+```
+
+---
+
+## Summary
+
+**Services** = Cơ sở API calls, có thể tái sử dụng ở bất kỳ đâu
+**Hooks** = Lớp React trên services, tối ưu cho React components
+
+**Dùng Services** khi bạn cần tính linh hoạt và độc lập với React
+**Dùng Hooks** khi bạn muốn TanStack Query quản lý state và caching tự động
diff --git a/src/App.css b/src/App.css
deleted file mode 100644
index 74b5e05..0000000
--- a/src/App.css
+++ /dev/null
@@ -1,38 +0,0 @@
-.App {
- text-align: center;
-}
-
-.App-logo {
- height: 40vmin;
- pointer-events: none;
-}
-
-@media (prefers-reduced-motion: no-preference) {
- .App-logo {
- animation: App-logo-spin infinite 20s linear;
- }
-}
-
-.App-header {
- background-color: #282c34;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- font-size: calc(10px + 2vmin);
- color: white;
-}
-
-.App-link {
- color: #61dafb;
-}
-
-@keyframes App-logo-spin {
- from {
- transform: rotate(0deg);
- }
- to {
- transform: rotate(360deg);
- }
-}
diff --git a/src/components/pages/error-route.tsx b/src/components/pages/error-route.tsx
new file mode 100644
index 0000000..fc27d39
--- /dev/null
+++ b/src/components/pages/error-route.tsx
@@ -0,0 +1,24 @@
+import { AlertTriangle } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Link } from "@tanstack/react-router";
+
+export default function ErrorRoute({ error }: { error: string }) {
+ return (
+
+
+
+
Lỗi
+
+ Đã xảy ra lỗi: {error}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/pages/not-found.tsx b/src/components/pages/not-found.tsx
new file mode 100644
index 0000000..bf8a62a
--- /dev/null
+++ b/src/components/pages/not-found.tsx
@@ -0,0 +1,35 @@
+import { Button } from "@/components/ui/button";
+import { Link } from "@tanstack/react-router";
+import { ArrowLeft, Search } from "lucide-react";
+
+export default function NotFound() {
+ return (
+
+
+
+
+
+
404
+
Không tìm thấy
+
+ Trang bạn yêu cầu truy cập không tồn tại hoặc đã bị xoá.
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/config/api.ts b/src/config/api.ts
index 5f38b58..e3fac17 100644
--- a/src/config/api.ts
+++ b/src/config/api.ts
@@ -5,6 +5,15 @@ export const BASE_URL = isDev
: "/api";
export const API_ENDPOINTS = {
+ AUTH: {
+ LOGIN: `${BASE_URL}/login`,
+ LOGOUT: `${BASE_URL}/logout`,
+ CHANGE_PASSWORD: `${BASE_URL}/auth/change-password`,
+ CHANGE_PASSWORD_ADMIN: `${BASE_URL}/auth/admin/change-password`,
+ PING: `${BASE_URL}/ping`,
+ CSRF_TOKEN: `${BASE_URL}/csrf-token`,
+ CREATE_ACCOUNT: `${BASE_URL}/auth/create-account`,
+ },
APP_VERSION: {
//agent and app api
GET_VERSION: `${BASE_URL}/AppVersion/version`,
diff --git a/src/hooks/queries/index.ts b/src/hooks/queries/index.ts
new file mode 100644
index 0000000..222c0e1
--- /dev/null
+++ b/src/hooks/queries/index.ts
@@ -0,0 +1,11 @@
+// Auth Queries
+export * from "./useAuthQueries";
+
+// App Version Queries
+export * from "./useAppVersionQueries";
+
+// Device Communication Queries
+export * from "./useDeviceCommQueries";
+
+// Command Queries
+export * from "./useCommandQueries";
diff --git a/src/hooks/queries/useAppVersionQueries.ts b/src/hooks/queries/useAppVersionQueries.ts
new file mode 100644
index 0000000..82da2be
--- /dev/null
+++ b/src/hooks/queries/useAppVersionQueries.ts
@@ -0,0 +1,186 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import * as appVersionService from "@/services/app-version.service";
+import type { AxiosProgressEvent } from "axios";
+import type { Version } from "@/types/file";
+
+const APP_VERSION_QUERY_KEYS = {
+ all: ["app-version"] as const,
+ agentVersion: () => [...APP_VERSION_QUERY_KEYS.all, "agent"] as const,
+ softwareList: () => [...APP_VERSION_QUERY_KEYS.all, "software"] as const,
+ blacklist: () => [...APP_VERSION_QUERY_KEYS.all, "blacklist"] as const,
+ requiredFiles: () => [...APP_VERSION_QUERY_KEYS.all, "required-files"] as const,
+};
+
+/**
+ * Hook để lấy danh sách phiên bản agent
+ */
+export function useGetAgentVersion(enabled = true) {
+ return useQuery({
+ queryKey: APP_VERSION_QUERY_KEYS.agentVersion(),
+ queryFn: () => appVersionService.getAgentVersion(),
+ enabled,
+ staleTime: 60 * 1000, // 1 minute
+ });
+}
+
+/**
+ * Hook để lấy danh sách phần mềm
+ */
+export function useGetSoftwareList(enabled = true) {
+ return useQuery({
+ queryKey: APP_VERSION_QUERY_KEYS.softwareList(),
+ queryFn: () => appVersionService.getSoftwareList(),
+ enabled,
+ staleTime: 60 * 1000,
+ });
+}
+
+/**
+ * Hook để upload file
+ */
+export function useUploadSoftware() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data: {
+ formData: FormData;
+ onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
+ }) => appVersionService.uploadSoftware(data.formData, data.onUploadProgress),
+ onSuccess: () => {
+ // Invalidate software list
+ queryClient.invalidateQueries({
+ queryKey: APP_VERSION_QUERY_KEYS.softwareList(),
+ });
+ },
+ });
+}
+
+/**
+ * Hook để lấy danh sách blacklist
+ */
+export function useGetBlacklist(enabled = true) {
+ return useQuery({
+ queryKey: APP_VERSION_QUERY_KEYS.blacklist(),
+ queryFn: () => appVersionService.getBlacklist(),
+ enabled,
+ staleTime: 60 * 1000,
+ });
+}
+
+/**
+ * Hook để thêm vào blacklist
+ */
+export function useAddBlacklist() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data: any) => appVersionService.addBlacklist(data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: APP_VERSION_QUERY_KEYS.blacklist(),
+ });
+ },
+ });
+}
+
+/**
+ * Hook để xóa khỏi blacklist
+ */
+export function useDeleteBlacklist() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (appId: number) => appVersionService.deleteBlacklist(appId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: APP_VERSION_QUERY_KEYS.blacklist(),
+ });
+ },
+ });
+}
+
+/**
+ * Hook để cập nhật blacklist
+ */
+export function useUpdateBlacklist() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({ appId, data }: { appId: string; data: any }) =>
+ appVersionService.updateBlacklist(appId, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: APP_VERSION_QUERY_KEYS.blacklist(),
+ });
+ },
+ });
+}
+
+/**
+ * Hook để yêu cầu cập nhật blacklist
+ */
+export function useRequestUpdateBlacklist() {
+ return useMutation({
+ mutationFn: (data: any) => appVersionService.requestUpdateBlacklist(data),
+ });
+}
+
+/**
+ * Hook để lấy danh sách file bắt buộc
+ */
+export function useGetRequiredFiles(enabled = true) {
+ return useQuery({
+ queryKey: APP_VERSION_QUERY_KEYS.requiredFiles(),
+ queryFn: () => appVersionService.getRequiredFiles(),
+ enabled,
+ staleTime: 60 * 1000,
+ });
+}
+
+/**
+ * Hook để thêm file bắt buộc
+ */
+export function useAddRequiredFile() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data: any) => appVersionService.addRequiredFile(data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: APP_VERSION_QUERY_KEYS.requiredFiles(),
+ });
+ },
+ });
+}
+
+/**
+ * Hook để xóa file bắt buộc
+ */
+export function useDeleteRequiredFile() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (fileId: number) => appVersionService.deleteRequiredFile(fileId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: APP_VERSION_QUERY_KEYS.requiredFiles(),
+ });
+ },
+ });
+}
+
+/**
+ * Hook để xóa file
+ */
+export function useDeleteFile() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (fileId: number) => appVersionService.deleteFile(fileId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: APP_VERSION_QUERY_KEYS.softwareList(),
+ });
+ },
+ });
+}
diff --git a/src/hooks/queries/useAuthQueries.ts b/src/hooks/queries/useAuthQueries.ts
new file mode 100644
index 0000000..a81e519
--- /dev/null
+++ b/src/hooks/queries/useAuthQueries.ts
@@ -0,0 +1,120 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import * as authService from "@/services/auth.service";
+import type { LoginResquest, LoginResponse } from "@/types/auth";
+
+const AUTH_QUERY_KEYS = {
+ all: ["auth"] as const,
+ ping: () => [...AUTH_QUERY_KEYS.all, "ping"] as const,
+ csrfToken: () => [...AUTH_QUERY_KEYS.all, "csrf-token"] as const,
+};
+
+/**
+ * Hook để đăng nhập
+ */
+export function useLogin() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (credentials) => authService.login(credentials),
+ onSuccess: (data) => {
+ // Lưu vào localStorage
+ if (data.token) {
+ localStorage.setItem("token", data.token);
+ localStorage.setItem("username", data.username || "");
+ localStorage.setItem("name", data.name || "");
+ localStorage.setItem("acs", JSON.stringify(data.access || []));
+ localStorage.setItem("role", data.role?.roleName || "");
+ localStorage.setItem("priority", String(data.role?.priority || "-1"));
+ }
+ // Invalidate ping query
+ queryClient.invalidateQueries({ queryKey: AUTH_QUERY_KEYS.ping() });
+ },
+ });
+}
+
+/**
+ * Hook để đăng xuất
+ */
+export function useLogout() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: () => authService.logout(),
+ onSuccess: () => {
+ localStorage.removeItem("token");
+ localStorage.removeItem("username");
+ localStorage.removeItem("name");
+ localStorage.removeItem("acs");
+ localStorage.removeItem("role");
+ localStorage.removeItem("priority");
+ // Clear all queries
+ queryClient.clear();
+ },
+ });
+}
+
+/**
+ * Hook để kiểm tra phiên đăng nhập
+ */
+export function usePing(token?: string, enabled = true) {
+ return useQuery({
+ queryKey: AUTH_QUERY_KEYS.ping(),
+ queryFn: () => authService.ping(token),
+ enabled,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ retry: 1,
+ });
+}
+
+/**
+ * Hook để lấy CSRF token
+ */
+export function useGetCsrfToken(enabled = true) {
+ return useQuery({
+ queryKey: AUTH_QUERY_KEYS.csrfToken(),
+ queryFn: () => authService.getCsrfToken(),
+ enabled,
+ staleTime: Infinity,
+ });
+}
+
+/**
+ * Hook để thay đổi mật khẩu
+ */
+export function useChangePassword() {
+ return useMutation({
+ mutationFn: (data: { currentPassword: string; newPassword: string }) =>
+ authService.changePassword(data),
+ });
+}
+
+/**
+ * Hook để admin thay đổi mật khẩu user khác
+ */
+export function useChangePasswordAdmin() {
+ return useMutation({
+ mutationFn: (data: { username: string; newPassword: string }) =>
+ authService.changePasswordAdmin(data),
+ });
+}
+
+/**
+ * Hook để tạo tài khoản mới
+ */
+export function useCreateAccount() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data: {
+ userName: string;
+ password: string;
+ name: string;
+ roleId: number;
+ accessBuildings?: number[];
+ }) => authService.createAccount(data),
+ onSuccess: () => {
+ // Có thể invalidate user list query nếu có
+ queryClient.invalidateQueries({ queryKey: ["users"] });
+ },
+ });
+}
diff --git a/src/hooks/queries/useCommandQueries.ts b/src/hooks/queries/useCommandQueries.ts
new file mode 100644
index 0000000..3e14346
--- /dev/null
+++ b/src/hooks/queries/useCommandQueries.ts
@@ -0,0 +1,74 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import * as commandService from "@/services/command.service";
+
+const COMMAND_QUERY_KEYS = {
+ all: ["commands"] as const,
+ list: () => [...COMMAND_QUERY_KEYS.all, "list"] as const,
+ detail: (id: number) => [...COMMAND_QUERY_KEYS.all, "detail", id] as const,
+};
+
+/**
+ * Hook để lấy danh sách lệnh
+ */
+export function useGetCommandList(enabled = true) {
+ return useQuery({
+ queryKey: COMMAND_QUERY_KEYS.list(),
+ queryFn: () => commandService.getCommandList(),
+ enabled,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ });
+}
+
+/**
+ * Hook để thêm lệnh mới
+ */
+export function useAddCommand() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data: any) => commandService.addCommand(data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: COMMAND_QUERY_KEYS.list(),
+ });
+ },
+ });
+}
+
+/**
+ * Hook để cập nhật lệnh
+ */
+export function useUpdateCommand() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({
+ commandId,
+ data,
+ }: {
+ commandId: number;
+ data: any;
+ }) => commandService.updateCommand(commandId, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: COMMAND_QUERY_KEYS.list(),
+ });
+ },
+ });
+}
+
+/**
+ * Hook để xóa lệnh
+ */
+export function useDeleteCommand() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (commandId: number) => commandService.deleteCommand(commandId),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: COMMAND_QUERY_KEYS.list(),
+ });
+ },
+ });
+}
diff --git a/src/hooks/queries/useDeviceCommQueries.ts b/src/hooks/queries/useDeviceCommQueries.ts
new file mode 100644
index 0000000..f938f01
--- /dev/null
+++ b/src/hooks/queries/useDeviceCommQueries.ts
@@ -0,0 +1,172 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+import * as deviceCommService from "@/services/device-comm.service";
+import type { DeviceHealthCheck } from "@/types/device";
+
+const DEVICE_COMM_QUERY_KEYS = {
+ all: ["device-comm"] as const,
+ allDevices: () => [...DEVICE_COMM_QUERY_KEYS.all, "all"] as const,
+ roomList: () => [...DEVICE_COMM_QUERY_KEYS.all, "rooms"] as const,
+ devicesInRoom: (roomName: string) =>
+ [...DEVICE_COMM_QUERY_KEYS.all, "room", roomName] as const,
+ clientFolderStatus: (roomName: string) =>
+ [...DEVICE_COMM_QUERY_KEYS.all, "folder-status", roomName] as const,
+};
+
+/**
+ * Hook để lấy tất cả thiết bị
+ */
+export function useGetAllDevices(enabled = true) {
+ return useQuery({
+ queryKey: DEVICE_COMM_QUERY_KEYS.allDevices(),
+ queryFn: () => deviceCommService.getAllDevices(),
+ enabled,
+ staleTime: 60 * 1000,
+ });
+}
+
+/**
+ * Hook để lấy danh sách phòng
+ */
+export function useGetRoomList(enabled = true) {
+ return useQuery({
+ queryKey: DEVICE_COMM_QUERY_KEYS.roomList(),
+ queryFn: () => deviceCommService.getRoomList(),
+ enabled,
+ staleTime: 5 * 60 * 1000, // 5 minutes
+ });
+}
+
+/**
+ * Hook để lấy danh sách thiết bị trong phòng
+ */
+export function useGetDeviceFromRoom(roomName?: string, enabled = true) {
+ return useQuery({
+ queryKey: roomName ? DEVICE_COMM_QUERY_KEYS.devicesInRoom(roomName) : ["disabled"],
+ queryFn: () =>
+ roomName ? deviceCommService.getDeviceFromRoom(roomName) : Promise.reject("No room"),
+ enabled: enabled && !!roomName,
+ staleTime: 30 * 1000, // 30 seconds
+ });
+}
+
+/**
+ * Hook để tải file
+ */
+export function useDownloadFiles() {
+ return useMutation({
+ mutationFn: ({
+ roomName,
+ data,
+ }: {
+ roomName: string;
+ data: any;
+ }) => deviceCommService.downloadFiles(roomName, data),
+ });
+}
+
+/**
+ * Hook để cài đặt MSI
+ */
+export function useInstallMsi() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({
+ roomName,
+ data,
+ }: {
+ roomName: string;
+ data: any;
+ }) => deviceCommService.installMsi(roomName, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: DEVICE_COMM_QUERY_KEYS.all,
+ });
+ },
+ });
+}
+
+/**
+ * Hook để cập nhật agent
+ */
+export function useUpdateAgent() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: ({
+ roomName,
+ data,
+ }: {
+ roomName: string;
+ data: any;
+ }) => deviceCommService.updateAgent(roomName, data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: DEVICE_COMM_QUERY_KEYS.all,
+ });
+ },
+ });
+}
+
+/**
+ * Hook để cập nhật blacklist
+ */
+export function useUpdateDeviceBlacklist() {
+ return useMutation({
+ mutationFn: ({
+ roomName,
+ data,
+ }: {
+ roomName: string;
+ data: any;
+ }) => deviceCommService.updateBlacklist(roomName, data),
+ });
+}
+
+/**
+ * Hook để gửi lệnh shell
+ */
+export function useSendCommand() {
+ return useMutation({
+ mutationFn: ({
+ roomName,
+ data,
+ }: {
+ roomName: string;
+ data: any;
+ }) => deviceCommService.sendCommand(roomName, data),
+ });
+}
+
+/**
+ * Hook để thay đổi phòng của thiết bị
+ */
+export function useChangeDeviceRoom() {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: (data: any) => deviceCommService.changeDeviceRoom(data),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: DEVICE_COMM_QUERY_KEYS.all,
+ });
+ },
+ });
+}
+
+/**
+ * Hook để lấy trạng thái folder client
+ */
+export function useGetClientFolderStatus(roomName?: string, enabled = true) {
+ return useQuery({
+ queryKey: roomName
+ ? DEVICE_COMM_QUERY_KEYS.clientFolderStatus(roomName)
+ : ["disabled"],
+ queryFn: () =>
+ roomName
+ ? deviceCommService.getClientFolderStatus(roomName)
+ : Promise.reject("No room"),
+ enabled: enabled && !!roomName,
+ staleTime: 30 * 1000,
+ });
+}
diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx
new file mode 100644
index 0000000..be153ff
--- /dev/null
+++ b/src/hooks/useAuth.tsx
@@ -0,0 +1,106 @@
+import { sleep } from "@/lib/utils";
+import { PermissionEnum } from "@/types/permission";
+import React, { useContext, useEffect, useMemo } from "react";
+import { useCallback, useState } from "react";
+
+export interface IAuthContext {
+ isAuthenticated: boolean;
+ setAuthenticated: (value: boolean) => void;
+ logout: () => Promise;
+ login: (username: string) => void;
+ username: string;
+ token: string;
+ name: string;
+ acs: number[];
+ hasPermission: (permission: PermissionEnum) => boolean;
+ role: {
+ roleName: string;
+ priority: number;
+ };
+}
+
+const AuthContext = React.createContext(null);
+
+const key = "accesscontrol.auth.user";
+
+function getStoredUser() {
+ return localStorage.getItem(key);
+}
+
+function setStoredUser(user: string | null) {
+ if (user) {
+ localStorage.setItem(key, user);
+ } else {
+ localStorage.removeItem(key);
+ }
+}
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ const [user, setUser] = useState(getStoredUser() || "");
+ const [isAuthenticated, setIsAuthenticated] = useState(!!user);
+ const token = localStorage.getItem("token") || "";
+ const name = localStorage.getItem("name") || "";
+ const acsString = localStorage.getItem("acs");
+ const acs = useMemo(() => (acsString ? acsString.split(",").map(Number) : []), [acsString]);
+ const roleName = localStorage.getItem("role") || "";
+ const priority = localStorage.getItem("priority") || "-1";
+
+ const setAuthenticated = useCallback((value: boolean) => {
+ setIsAuthenticated(value);
+ }, []);
+
+ const login = useCallback((username: string) => {
+ setStoredUser(username);
+ setUser(username);
+ }, []);
+
+ const hasPermission = useCallback(
+ (permission: PermissionEnum) => {
+ return acs.some((a) => a === permission);
+ },
+ [acs]
+ );
+
+ const logout = useCallback(async () => {
+ await sleep(250);
+ setAuthenticated(false);
+ setStoredUser("");
+ setUser("");
+ localStorage.removeItem("token");
+ localStorage.removeItem("name");
+ localStorage.removeItem("acs");
+ localStorage.removeItem("role");
+ localStorage.removeItem("priority");
+ }, [setAuthenticated]);
+
+ useEffect(() => {
+ setUser(getStoredUser() || "");
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error("useAuth must be used within an AuthProvider");
+ }
+ return context;
+}
diff --git a/src/hooks/useDeleteData.ts b/src/hooks/useDeleteData.ts
deleted file mode 100644
index 3ec2b8d..0000000
--- a/src/hooks/useDeleteData.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import axios from "axios";
-
-type DeleteDataOptions = {
- onSuccess?: (data: TOutput) => void;
- onError?: (error: any) => void;
- invalidate?: string[][];
-};
-
-export function useDeleteData({
- onSuccess,
- onError,
- invalidate = [],
-}: DeleteDataOptions = {}) {
- const queryClient = useQueryClient();
-
- return useMutation<
- TOutput,
- any,
- {
- url: string;
- config?: any;
- }
- >({
- mutationFn: async ({ url, config }) => {
- const response = await axios.delete(url, config);
- return response.data;
- },
- onSuccess: (data) => {
- invalidate.forEach((key) =>
- queryClient.invalidateQueries({ queryKey: key })
- );
- onSuccess?.(data);
- },
- onError,
- });
-}
diff --git a/src/hooks/useMutationData.ts b/src/hooks/useMutationData.ts
deleted file mode 100644
index 27dfb1b..0000000
--- a/src/hooks/useMutationData.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { useMutation, useQueryClient } from "@tanstack/react-query";
-import axios, { type Method } from "axios";
-
-type MutationDataOptions = {
- url: string;
- method?: Method;
- onSuccess?: (data: TOutput) => void;
- onError?: (error: any) => void;
- invalidate?: string[][];
-};
-
-export function useMutationData({
- url,
- method = "POST",
- onSuccess,
- onError,
- invalidate = [],
-}: MutationDataOptions) {
- const queryClient = useQueryClient();
-
- return useMutation<
- TOutput,
- any,
- { data: TInput; url?: string; config?: any; method?: Method }
- >({
- mutationFn: async ({
- data,
- config,
- url: customUrl,
- method: customMethod,
- }) => {
- const isFormData = data instanceof FormData;
-
- const response = await axios.request({
- url: customUrl ?? url,
- method: customMethod ?? method,
- data,
- headers: {
- ...(isFormData ? {} : { "Content-Type": "application/json" }),
- },
- ...config,
- });
- return response.data;
- },
- onSuccess: async (data) => {
- // Invalidate queries trước
- await Promise.all(
- invalidate.map((key) =>
- queryClient.invalidateQueries({ queryKey: key })
- )
- );
- // Sau đó gọi callback
- onSuccess?.(data);
- },
- onError,
- });
-}
diff --git a/src/hooks/useQueryData.ts b/src/hooks/useQueryData.ts
deleted file mode 100644
index 5fce8cf..0000000
--- a/src/hooks/useQueryData.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useQuery } from "@tanstack/react-query";
-import axios from "axios";
-
-type QueryDataOptions = {
- queryKey: string[];
- url: string;
- params?: Record;
- select?: (data: any) => T;
- enabled?: boolean;
-};
-
-export function useQueryData({
- queryKey,
- url,
- params,
- select,
- enabled = true,
-}: QueryDataOptions) {
- return useQuery({
- queryKey,
- queryFn: () => axios.get(url, { params }).then((res) => res.data),
- select,
- enabled,
- });
-}
diff --git a/src/styles.css b/src/index.css
similarity index 100%
rename from src/styles.css
rename to src/index.css
diff --git a/src/layouts/app-layout.tsx b/src/layouts/app-layout.tsx
index 7a33b1e..e269118 100644
--- a/src/layouts/app-layout.tsx
+++ b/src/layouts/app-layout.tsx
@@ -8,8 +8,13 @@ import {
import { Home, Building, AppWindow, Terminal, CircleX } from "lucide-react";
import { Toaster } from "@/components/ui/sonner";
import { useQueryClient } from "@tanstack/react-query";
-import { API_ENDPOINTS } from "@/config/api";
import { Separator } from "@/components/ui/separator";
+import {
+ useGetAgentVersion,
+ useGetSoftwareList,
+ useGetRoomList,
+ useGetBlacklist,
+} from "@/hooks/queries";
type AppLayoutProps = {
children: ReactNode;
@@ -20,43 +25,32 @@ export default function AppLayout({ children }: AppLayoutProps) {
const handlePrefetchAgents = () => {
queryClient.prefetchQuery({
- queryKey: ["agent-version"],
- queryFn: () =>
- fetch(API_ENDPOINTS.APP_VERSION.GET_VERSION).then((res) =>
- res.json()
- ),
+ queryKey: ["app-version", "agent"],
+ queryFn: useGetAgentVersion as any,
staleTime: 60 * 1000,
});
};
+
const handlePrefetchSofware = () => {
queryClient.prefetchQuery({
- queryKey: ["software-version"],
- queryFn: () =>
- fetch(API_ENDPOINTS.APP_VERSION.GET_SOFTWARE).then((res) =>
- res.json()
- ),
+ queryKey: ["app-version", "software"],
+ queryFn: useGetSoftwareList as any,
staleTime: 60 * 1000,
});
};
const handlePrefetchRooms = () => {
queryClient.prefetchQuery({
- queryKey: ["room-list"],
- queryFn: () =>
- fetch(API_ENDPOINTS.DEVICE_COMM.GET_ROOM_LIST).then((res) =>
- res.json()
- ),
- staleTime: 60 * 1000,
+ queryKey: ["device-comm", "rooms"],
+ queryFn: useGetRoomList as any,
+ staleTime: 5 * 60 * 1000,
});
};
const handlePrefetchBannedSoftware = () => {
queryClient.prefetchQuery({
- queryKey: ["blacklist"],
- queryFn: () =>
- fetch(API_ENDPOINTS.APP_VERSION + "").then((res) =>
- res.json()
- ),
+ queryKey: ["app-version", "blacklist"],
+ queryFn: useGetBlacklist as any,
staleTime: 60 * 1000,
});
};
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index bd0c391..d34bbd9 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -4,3 +4,7 @@ import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
+
+export function sleep(ms: number): Promise {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
diff --git a/src/main.tsx b/src/main.tsx
index b2587f7..e46d9be 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,25 +1,24 @@
+/* eslint-disable react-refresh/only-export-components */
import { StrictMode } from "react";
+import "./index.css";
import ReactDOM from "react-dom/client";
import { RouterProvider, createRouter } from "@tanstack/react-router";
-import useAuthToken from "./hooks/useAuthtoken";
-import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
// Import the generated route tree
import { routeTree } from "./routeTree.gen";
-
-import "./styles.css";
-
-const auth = useAuthToken.getState();
-
-export const queryClient = new QueryClient();
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import axios from "axios";
+import { AuthProvider, useAuth } from "@/hooks/useAuth";
+import { toast, Toaster } from "sonner";
// Create a new router instance
const router = createRouter({
routeTree,
- context: { auth },
defaultPreload: "intent",
scrollRestoration: true,
- defaultStructuralSharing: true,
- defaultPreloadStaleTime: 0,
+ context: {
+ auth: undefined!, // This will be set after we initialize the auth store
+ queryClient: undefined!
+ }
});
// Register the router instance for type safety
@@ -27,18 +26,61 @@ declare module "@tanstack/react-router" {
interface Register {
router: typeof router;
}
+ interface HistoryState {
+ name?: string;
+ }
+}
+
+function InnerApp() {
+ const auth = useAuth();
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ retry: (failureCount, error: unknown) => {
+ if (axios.isAxiosError(error)) {
+ if (error.response?.status === 401) {
+ if (error.response?.data.message != null) {
+ toast.error("Không có quyền truy cập!");
+ return false;
+ } else {
+ auth.logout();
+ queryClient.invalidateQueries();
+ queryClient.clear();
+ return false;
+ }
+ }
+ }
+ return failureCount < 3;
+ }
+ }
+ }
+ });
+ return (
+
+
+
+ );
+}
+
+function App() {
+ return (
+
+
+
+ );
}
// Render the app
const rootElement = document.getElementById("app");
-if (rootElement && !rootElement.innerHTML) {
- const root = ReactDOM.createRoot(rootElement);
- root.render(
-
-
- {" "}
-
-
-
- );
+
+if (!rootElement) {
+ throw new Error("Failed to find the root element");
}
+
+const root = ReactDOM.createRoot(rootElement);
+root.render(
+
+
+
+
+);
diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts
index bc97134..2ed4a75 100644
--- a/src/routeTree.gen.ts
+++ b/src/routeTree.gen.ts
@@ -9,22 +9,18 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
-import { Route as AuthenticatedRouteImport } from './routes/_authenticated'
import { Route as AuthRouteImport } from './routes/_auth'
import { Route as IndexRouteImport } from './routes/index'
-import { Route as AuthenticatedRoomIndexRouteImport } from './routes/_authenticated/room/index'
-import { Route as AuthenticatedDeviceIndexRouteImport } from './routes/_authenticated/device/index'
-import { Route as AuthenticatedCommandIndexRouteImport } from './routes/_authenticated/command/index'
-import { Route as AuthenticatedBlacklistIndexRouteImport } from './routes/_authenticated/blacklist/index'
-import { Route as AuthenticatedAppsIndexRouteImport } from './routes/_authenticated/apps/index'
-import { Route as AuthenticatedAgentIndexRouteImport } from './routes/_authenticated/agent/index'
-import { Route as AuthLoginIndexRouteImport } from './routes/_auth/login/index'
-import { Route as AuthenticatedRoomRoomNameIndexRouteImport } from './routes/_authenticated/room/$roomName/index'
+import { Route as AuthRoomIndexRouteImport } from './routes/_auth/room/index'
+import { Route as AuthDeviceIndexRouteImport } from './routes/_auth/device/index'
+import { Route as AuthDashboardIndexRouteImport } from './routes/_auth/dashboard/index'
+import { Route as AuthCommandIndexRouteImport } from './routes/_auth/command/index'
+import { Route as AuthBlacklistIndexRouteImport } from './routes/_auth/blacklist/index'
+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 AuthRoomRoomNameIndexRouteImport } from './routes/_auth/room/$roomName/index'
-const AuthenticatedRoute = AuthenticatedRouteImport.update({
- id: '/_authenticated',
- getParentRoute: () => rootRouteImport,
-} as any)
const AuthRoute = AuthRouteImport.update({
id: '/_auth',
getParentRoute: () => rootRouteImport,
@@ -34,86 +30,89 @@ const IndexRoute = IndexRouteImport.update({
path: '/',
getParentRoute: () => rootRouteImport,
} as any)
-const AuthenticatedRoomIndexRoute = AuthenticatedRoomIndexRouteImport.update({
+const AuthRoomIndexRoute = AuthRoomIndexRouteImport.update({
id: '/room/',
path: '/room/',
- getParentRoute: () => AuthenticatedRoute,
-} as any)
-const AuthenticatedDeviceIndexRoute =
- AuthenticatedDeviceIndexRouteImport.update({
- id: '/device/',
- path: '/device/',
- getParentRoute: () => AuthenticatedRoute,
- } as any)
-const AuthenticatedCommandIndexRoute =
- AuthenticatedCommandIndexRouteImport.update({
- id: '/command/',
- path: '/command/',
- getParentRoute: () => AuthenticatedRoute,
- } as any)
-const AuthenticatedBlacklistIndexRoute =
- AuthenticatedBlacklistIndexRouteImport.update({
- id: '/blacklist/',
- path: '/blacklist/',
- getParentRoute: () => AuthenticatedRoute,
- } as any)
-const AuthenticatedAppsIndexRoute = AuthenticatedAppsIndexRouteImport.update({
- id: '/apps/',
- path: '/apps/',
- getParentRoute: () => AuthenticatedRoute,
-} as any)
-const AuthenticatedAgentIndexRoute = AuthenticatedAgentIndexRouteImport.update({
- id: '/agent/',
- path: '/agent/',
- getParentRoute: () => AuthenticatedRoute,
-} as any)
-const AuthLoginIndexRoute = AuthLoginIndexRouteImport.update({
- id: '/login/',
- path: '/login/',
getParentRoute: () => AuthRoute,
} as any)
-const AuthenticatedRoomRoomNameIndexRoute =
- AuthenticatedRoomRoomNameIndexRouteImport.update({
- id: '/room/$roomName/',
- path: '/room/$roomName/',
- getParentRoute: () => AuthenticatedRoute,
- } as any)
+const AuthDeviceIndexRoute = AuthDeviceIndexRouteImport.update({
+ id: '/device/',
+ path: '/device/',
+ getParentRoute: () => AuthRoute,
+} as any)
+const AuthDashboardIndexRoute = AuthDashboardIndexRouteImport.update({
+ id: '/dashboard/',
+ path: '/dashboard/',
+ getParentRoute: () => AuthRoute,
+} as any)
+const AuthCommandIndexRoute = AuthCommandIndexRouteImport.update({
+ id: '/command/',
+ path: '/command/',
+ getParentRoute: () => AuthRoute,
+} as any)
+const AuthBlacklistIndexRoute = AuthBlacklistIndexRouteImport.update({
+ id: '/blacklist/',
+ path: '/blacklist/',
+ getParentRoute: () => AuthRoute,
+} as any)
+const AuthAppsIndexRoute = AuthAppsIndexRouteImport.update({
+ id: '/apps/',
+ path: '/apps/',
+ getParentRoute: () => AuthRoute,
+} as any)
+const AuthAgentIndexRoute = AuthAgentIndexRouteImport.update({
+ id: '/agent/',
+ path: '/agent/',
+ getParentRoute: () => AuthRoute,
+} as any)
+const authLoginIndexRoute = authLoginIndexRouteImport.update({
+ id: '/(auth)/login/',
+ path: '/login/',
+ getParentRoute: () => rootRouteImport,
+} as any)
+const AuthRoomRoomNameIndexRoute = AuthRoomRoomNameIndexRouteImport.update({
+ id: '/room/$roomName/',
+ path: '/room/$roomName/',
+ getParentRoute: () => AuthRoute,
+} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
- '/login': typeof AuthLoginIndexRoute
- '/agent': typeof AuthenticatedAgentIndexRoute
- '/apps': typeof AuthenticatedAppsIndexRoute
- '/blacklist': typeof AuthenticatedBlacklistIndexRoute
- '/command': typeof AuthenticatedCommandIndexRoute
- '/device': typeof AuthenticatedDeviceIndexRoute
- '/room': typeof AuthenticatedRoomIndexRoute
- '/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
+ '/login': typeof authLoginIndexRoute
+ '/agent': typeof AuthAgentIndexRoute
+ '/apps': typeof AuthAppsIndexRoute
+ '/blacklist': typeof AuthBlacklistIndexRoute
+ '/command': typeof AuthCommandIndexRoute
+ '/dashboard': typeof AuthDashboardIndexRoute
+ '/device': typeof AuthDeviceIndexRoute
+ '/room': typeof AuthRoomIndexRoute
+ '/room/$roomName': typeof AuthRoomRoomNameIndexRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
- '/login': typeof AuthLoginIndexRoute
- '/agent': typeof AuthenticatedAgentIndexRoute
- '/apps': typeof AuthenticatedAppsIndexRoute
- '/blacklist': typeof AuthenticatedBlacklistIndexRoute
- '/command': typeof AuthenticatedCommandIndexRoute
- '/device': typeof AuthenticatedDeviceIndexRoute
- '/room': typeof AuthenticatedRoomIndexRoute
- '/room/$roomName': typeof AuthenticatedRoomRoomNameIndexRoute
+ '/login': typeof authLoginIndexRoute
+ '/agent': typeof AuthAgentIndexRoute
+ '/apps': typeof AuthAppsIndexRoute
+ '/blacklist': typeof AuthBlacklistIndexRoute
+ '/command': typeof AuthCommandIndexRoute
+ '/dashboard': typeof AuthDashboardIndexRoute
+ '/device': typeof AuthDeviceIndexRoute
+ '/room': typeof AuthRoomIndexRoute
+ '/room/$roomName': typeof AuthRoomRoomNameIndexRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/_auth': typeof AuthRouteWithChildren
- '/_authenticated': typeof AuthenticatedRouteWithChildren
- '/_auth/login/': typeof AuthLoginIndexRoute
- '/_authenticated/agent/': typeof AuthenticatedAgentIndexRoute
- '/_authenticated/apps/': typeof AuthenticatedAppsIndexRoute
- '/_authenticated/blacklist/': typeof AuthenticatedBlacklistIndexRoute
- '/_authenticated/command/': typeof AuthenticatedCommandIndexRoute
- '/_authenticated/device/': typeof AuthenticatedDeviceIndexRoute
- '/_authenticated/room/': typeof AuthenticatedRoomIndexRoute
- '/_authenticated/room/$roomName/': typeof AuthenticatedRoomRoomNameIndexRoute
+ '/(auth)/login/': typeof authLoginIndexRoute
+ '/_auth/agent/': typeof AuthAgentIndexRoute
+ '/_auth/apps/': typeof AuthAppsIndexRoute
+ '/_auth/blacklist/': typeof AuthBlacklistIndexRoute
+ '/_auth/command/': typeof AuthCommandIndexRoute
+ '/_auth/dashboard/': typeof AuthDashboardIndexRoute
+ '/_auth/device/': typeof AuthDeviceIndexRoute
+ '/_auth/room/': typeof AuthRoomIndexRoute
+ '/_auth/room/$roomName/': typeof AuthRoomRoomNameIndexRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
@@ -124,6 +123,7 @@ export interface FileRouteTypes {
| '/apps'
| '/blacklist'
| '/command'
+ | '/dashboard'
| '/device'
| '/room'
| '/room/$roomName'
@@ -135,6 +135,7 @@ export interface FileRouteTypes {
| '/apps'
| '/blacklist'
| '/command'
+ | '/dashboard'
| '/device'
| '/room'
| '/room/$roomName'
@@ -142,32 +143,25 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/_auth'
- | '/_authenticated'
- | '/_auth/login/'
- | '/_authenticated/agent/'
- | '/_authenticated/apps/'
- | '/_authenticated/blacklist/'
- | '/_authenticated/command/'
- | '/_authenticated/device/'
- | '/_authenticated/room/'
- | '/_authenticated/room/$roomName/'
+ | '/(auth)/login/'
+ | '/_auth/agent/'
+ | '/_auth/apps/'
+ | '/_auth/blacklist/'
+ | '/_auth/command/'
+ | '/_auth/dashboard/'
+ | '/_auth/device/'
+ | '/_auth/room/'
+ | '/_auth/room/$roomName/'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AuthRoute: typeof AuthRouteWithChildren
- AuthenticatedRoute: typeof AuthenticatedRouteWithChildren
+ authLoginIndexRoute: typeof authLoginIndexRoute
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
- '/_authenticated': {
- id: '/_authenticated'
- path: ''
- fullPath: ''
- preLoaderRoute: typeof AuthenticatedRouteImport
- parentRoute: typeof rootRouteImport
- }
'/_auth': {
id: '/_auth'
path: ''
@@ -182,103 +176,100 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
- '/_authenticated/room/': {
- id: '/_authenticated/room/'
+ '/_auth/room/': {
+ id: '/_auth/room/'
path: '/room'
fullPath: '/room'
- preLoaderRoute: typeof AuthenticatedRoomIndexRouteImport
- parentRoute: typeof AuthenticatedRoute
- }
- '/_authenticated/device/': {
- id: '/_authenticated/device/'
- path: '/device'
- fullPath: '/device'
- preLoaderRoute: typeof AuthenticatedDeviceIndexRouteImport
- parentRoute: typeof AuthenticatedRoute
- }
- '/_authenticated/command/': {
- id: '/_authenticated/command/'
- path: '/command'
- fullPath: '/command'
- preLoaderRoute: typeof AuthenticatedCommandIndexRouteImport
- parentRoute: typeof AuthenticatedRoute
- }
- '/_authenticated/blacklist/': {
- id: '/_authenticated/blacklist/'
- path: '/blacklist'
- fullPath: '/blacklist'
- preLoaderRoute: typeof AuthenticatedBlacklistIndexRouteImport
- parentRoute: typeof AuthenticatedRoute
- }
- '/_authenticated/apps/': {
- id: '/_authenticated/apps/'
- path: '/apps'
- fullPath: '/apps'
- preLoaderRoute: typeof AuthenticatedAppsIndexRouteImport
- parentRoute: typeof AuthenticatedRoute
- }
- '/_authenticated/agent/': {
- id: '/_authenticated/agent/'
- path: '/agent'
- fullPath: '/agent'
- preLoaderRoute: typeof AuthenticatedAgentIndexRouteImport
- parentRoute: typeof AuthenticatedRoute
- }
- '/_auth/login/': {
- id: '/_auth/login/'
- path: '/login'
- fullPath: '/login'
- preLoaderRoute: typeof AuthLoginIndexRouteImport
+ preLoaderRoute: typeof AuthRoomIndexRouteImport
parentRoute: typeof AuthRoute
}
- '/_authenticated/room/$roomName/': {
- id: '/_authenticated/room/$roomName/'
+ '/_auth/device/': {
+ id: '/_auth/device/'
+ path: '/device'
+ fullPath: '/device'
+ preLoaderRoute: typeof AuthDeviceIndexRouteImport
+ parentRoute: typeof AuthRoute
+ }
+ '/_auth/dashboard/': {
+ id: '/_auth/dashboard/'
+ path: '/dashboard'
+ fullPath: '/dashboard'
+ preLoaderRoute: typeof AuthDashboardIndexRouteImport
+ parentRoute: typeof AuthRoute
+ }
+ '/_auth/command/': {
+ id: '/_auth/command/'
+ path: '/command'
+ fullPath: '/command'
+ preLoaderRoute: typeof AuthCommandIndexRouteImport
+ parentRoute: typeof AuthRoute
+ }
+ '/_auth/blacklist/': {
+ id: '/_auth/blacklist/'
+ path: '/blacklist'
+ fullPath: '/blacklist'
+ preLoaderRoute: typeof AuthBlacklistIndexRouteImport
+ parentRoute: typeof AuthRoute
+ }
+ '/_auth/apps/': {
+ id: '/_auth/apps/'
+ path: '/apps'
+ fullPath: '/apps'
+ preLoaderRoute: typeof AuthAppsIndexRouteImport
+ parentRoute: typeof AuthRoute
+ }
+ '/_auth/agent/': {
+ id: '/_auth/agent/'
+ path: '/agent'
+ fullPath: '/agent'
+ preLoaderRoute: typeof AuthAgentIndexRouteImport
+ parentRoute: typeof AuthRoute
+ }
+ '/(auth)/login/': {
+ id: '/(auth)/login/'
+ path: '/login'
+ fullPath: '/login'
+ preLoaderRoute: typeof authLoginIndexRouteImport
+ parentRoute: typeof rootRouteImport
+ }
+ '/_auth/room/$roomName/': {
+ id: '/_auth/room/$roomName/'
path: '/room/$roomName'
fullPath: '/room/$roomName'
- preLoaderRoute: typeof AuthenticatedRoomRoomNameIndexRouteImport
- parentRoute: typeof AuthenticatedRoute
+ preLoaderRoute: typeof AuthRoomRoomNameIndexRouteImport
+ parentRoute: typeof AuthRoute
}
}
}
interface AuthRouteChildren {
- AuthLoginIndexRoute: typeof AuthLoginIndexRoute
+ AuthAgentIndexRoute: typeof AuthAgentIndexRoute
+ AuthAppsIndexRoute: typeof AuthAppsIndexRoute
+ AuthBlacklistIndexRoute: typeof AuthBlacklistIndexRoute
+ AuthCommandIndexRoute: typeof AuthCommandIndexRoute
+ AuthDashboardIndexRoute: typeof AuthDashboardIndexRoute
+ AuthDeviceIndexRoute: typeof AuthDeviceIndexRoute
+ AuthRoomIndexRoute: typeof AuthRoomIndexRoute
+ AuthRoomRoomNameIndexRoute: typeof AuthRoomRoomNameIndexRoute
}
const AuthRouteChildren: AuthRouteChildren = {
- AuthLoginIndexRoute: AuthLoginIndexRoute,
+ AuthAgentIndexRoute: AuthAgentIndexRoute,
+ AuthAppsIndexRoute: AuthAppsIndexRoute,
+ AuthBlacklistIndexRoute: AuthBlacklistIndexRoute,
+ AuthCommandIndexRoute: AuthCommandIndexRoute,
+ AuthDashboardIndexRoute: AuthDashboardIndexRoute,
+ AuthDeviceIndexRoute: AuthDeviceIndexRoute,
+ AuthRoomIndexRoute: AuthRoomIndexRoute,
+ AuthRoomRoomNameIndexRoute: AuthRoomRoomNameIndexRoute,
}
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
-interface AuthenticatedRouteChildren {
- AuthenticatedAgentIndexRoute: typeof AuthenticatedAgentIndexRoute
- AuthenticatedAppsIndexRoute: typeof AuthenticatedAppsIndexRoute
- AuthenticatedBlacklistIndexRoute: typeof AuthenticatedBlacklistIndexRoute
- AuthenticatedCommandIndexRoute: typeof AuthenticatedCommandIndexRoute
- AuthenticatedDeviceIndexRoute: typeof AuthenticatedDeviceIndexRoute
- AuthenticatedRoomIndexRoute: typeof AuthenticatedRoomIndexRoute
- AuthenticatedRoomRoomNameIndexRoute: typeof AuthenticatedRoomRoomNameIndexRoute
-}
-
-const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
- AuthenticatedAgentIndexRoute: AuthenticatedAgentIndexRoute,
- AuthenticatedAppsIndexRoute: AuthenticatedAppsIndexRoute,
- AuthenticatedBlacklistIndexRoute: AuthenticatedBlacklistIndexRoute,
- AuthenticatedCommandIndexRoute: AuthenticatedCommandIndexRoute,
- AuthenticatedDeviceIndexRoute: AuthenticatedDeviceIndexRoute,
- AuthenticatedRoomIndexRoute: AuthenticatedRoomIndexRoute,
- AuthenticatedRoomRoomNameIndexRoute: AuthenticatedRoomRoomNameIndexRoute,
-}
-
-const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
- AuthenticatedRouteChildren,
-)
-
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AuthRoute: AuthRouteWithChildren,
- AuthenticatedRoute: AuthenticatedRouteWithChildren,
+ authLoginIndexRoute: authLoginIndexRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
diff --git a/src/routes/_auth/login/index.tsx b/src/routes/(auth)/login/index.tsx
similarity index 75%
rename from src/routes/_auth/login/index.tsx
rename to src/routes/(auth)/login/index.tsx
index e6d2e1a..e09a844 100644
--- a/src/routes/_auth/login/index.tsx
+++ b/src/routes/(auth)/login/index.tsx
@@ -1,4 +1,4 @@
-import { createFileRoute, redirect } from '@tanstack/react-router'
+import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
import {
Card,
CardContent,
@@ -14,6 +14,8 @@ import {
formOptions,
useForm,
} from '@tanstack/react-form'
+import { useLogin } from '@/hooks/queries'
+import { toast } from 'sonner'
interface LoginFormProps {
username: string
@@ -29,26 +31,34 @@ const formOpts = formOptions({
defaultValues: defaultInput,
})
-export const Route = createFileRoute('/_auth/login/')({
+export const Route = createFileRoute('/(auth)/login/')({
beforeLoad: async ({ context }) => {
- const { authToken } = context.auth
- if (authToken) throw redirect({ to: '/' })
+ const { token } = context.auth
+ if (token) throw redirect({ to: '/' })
},
component: LoginForm,
})
function LoginForm() {
+ const navigate = useNavigate()
+ const loginMutation = useLogin()
+
const form = useForm({
...formOpts,
onSubmit: async ({ value }) => {
- console.log('Submitting login form with values:', value)
+ try {
+ await loginMutation.mutateAsync({
+ username: value.username,
+ password: value.password,
+ })
- // Giả lập đăng nhập
- if (value.username === 'admin' && value.password === '123456') {
- alert('Đăng nhập thành công!')
- // Thêm xử lý lưu token, redirect...
- } else {
- alert('Tài khoản hoặc mật khẩu không đúng.')
+ toast.success('Đăng nhập thành công!')
+ navigate({ to: '/' })
+ } catch (error: any) {
+ console.error('Login error:', error)
+ toast.error(
+ error.response?.data?.message || 'Tài khoản hoặc mật khẩu không đúng.'
+ )
}
},
})
@@ -110,8 +120,8 @@ function LoginForm() {
)}
-