diff --git a/.env b/.env index 5bf9e95..a4fb8f9 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VITE_SV_HOST="http://localhost:3000" \ No newline at end of file +VITE_SV_HOST="http://localhost:3000" +VITE_JWT_TOKEN="phuocntbasdasasdasd" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d671642..eeb2682 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@tailwindcss/vite": "^4.1.13", "antd": "^5.27.4", "axios": "^1.12.2", + "jose": "^6.1.0", "json-server": "^1.0.0-beta.3", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -4157,6 +4158,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.0.tgz", + "integrity": "sha512-TTQJyoEoKcC1lscpVDCSsVgYzUDg/0Bt3WE//WiTPK6uOCQC2KZS4MpugbMWt/zyjkopgZoXhZuCi00gLudfUA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index b5aed0a..ff8cdfd 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@tailwindcss/vite": "^4.1.13", "antd": "^5.27.4", "axios": "^1.12.2", + "jose": "^6.1.0", "json-server": "^1.0.0-beta.3", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git a/src/apis/core/user.api.ts b/src/apis/core/user.api.ts index 69d9567..e2cb097 100644 --- a/src/apis/core/user.api.ts +++ b/src/apis/core/user.api.ts @@ -2,6 +2,7 @@ import axios from "axios" import type { User } from "../../types/user.type" import { message } from "antd" import { ApiUtil } from "../../utils/api.util" +import * as jose from 'jose' export interface UserSignInDTO { emailOrUserName: string @@ -16,10 +17,7 @@ export interface UserFindAllDTO { } export const UserApi = { - signIn: async (data: UserSignInDTO): Promise<{ - message: string - data: any - }> => { + signIn: async (data: UserSignInDTO) => { let userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?email=${data.emailOrUserName}`) if (userData.data.length == 0) { userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?userName=${data.emailOrUserName}`) @@ -36,10 +34,7 @@ export const UserApi = { data: null }) } - return { - message: "Đăng nhập thành công!", - data: userData.data[0] as User - } + return createToken(userData.data[0].id) } }, signUp: async (data: User) => { @@ -106,5 +101,59 @@ export const UserApi = { findAll: async (query?: UserFindAllDTO) => { let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?` + ApiUtil.writeQuery(query)) return result.data + }, + me: async (token: string) => { + let tokenData = await decodeToken(token) + + if (!tokenData) { + throw ({ + message: "Token không chính xác!" + }) + } + + let { userId } = tokenData; + + let getUserByIdRes = await axios.get(`${import.meta.env.VITE_SV_HOST}/users/${userId}`) + + if (!getUserByIdRes.data) { + throw ({ + message: "Lỗi lấy dữ liệu" + }) + } + + let data = await new Promise((resolve) => { + setTimeout(() => { + resolve(getUserByIdRes.data) + }, 1000) + }) + + return data + } +} + + +async function createToken(userId: string) { + const secret = new TextEncoder().encode(import.meta.env.VITE_JWT_TOKEN); + + const token = await new jose.SignJWT({ userId }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('2h') + .sign(secret); + return token +} + +async function decodeToken(token: string) { + try { + const secret = new TextEncoder().encode(import.meta.env.VITE_JWT_TOKEN); + + const { payload } = await jose.jwtVerify(token, secret, { + algorithms: ['HS256'], + }); + + return payload; + } catch (error) { + console.error('Token không hợp lệ hoặc đã hết hạn:', error); + return null; } } \ No newline at end of file diff --git a/src/pages/admin/auth/ProtectedAdmin.tsx b/src/pages/admin/auth/ProtectedAdmin.tsx index d4543ff..6d0d254 100644 --- a/src/pages/admin/auth/ProtectedAdmin.tsx +++ b/src/pages/admin/auth/ProtectedAdmin.tsx @@ -14,7 +14,7 @@ export default function ProtectedAdmin( return store.user }) - + /* Đã đăng nhập và là master hoặc admin */ if (userStore.data?.role == UserRole.MASTER || userStore.data?.role == UserRole.ADMIN) { return ( <> @@ -23,7 +23,8 @@ export default function ProtectedAdmin( ) } - if (!userStore.data?.role) { + /* chưa đăng nhập */ + if (!userStore.data && !userStore.loading) { return (
@@ -90,11 +91,81 @@ export default function ProtectedAdmin( ) } + /* đã đăng nhập nhưng không phải master hoặc admin */ if (userStore.data?.role) { - window.location.href="/" + window.location.href = "/" return <> } + + /* chưa vào case, default */ + return ( +
+
+ {/* avatar skeleton + spinner */} +
+
+ + + + +
+ + +
+
+
+
+
+ + +
+
+
+
+ + +
+
+ + + + +
+
+
+ + +
+ Đang xác thực — kết nối tới máy chủ... +
+ + + {/* visually-hidden for screen readers */} + Loading authentication information + + + +
+ ) + async function signInHandle(e: FormEvent) { e.preventDefault() let data: UserSignInDTO = { @@ -103,10 +174,9 @@ export default function ProtectedAdmin( } try { let result = await Apis.user.signIn(data) - localStorage.setItem("userLogin", JSON.stringify(result.data)) + localStorage.setItem("token", result) Modal.confirm({ title: "Đăng nhập thành công", - content: result.message, onOk: () => { window.location.reload() }, diff --git a/src/pages/admin/components/Header.tsx b/src/pages/admin/components/Header.tsx index 23ac9d5..6ca4e6a 100644 --- a/src/pages/admin/components/Header.tsx +++ b/src/pages/admin/components/Header.tsx @@ -28,7 +28,7 @@ export default function HeaderCom({ collapsed, setCollapsed }: { collapsed: bool diff --git a/src/pages/home/components/Header/Header.tsx b/src/pages/home/components/Header/Header.tsx index ebde218..8234030 100644 --- a/src/pages/home/components/Header/Header.tsx +++ b/src/pages/home/components/Header/Header.tsx @@ -70,7 +70,7 @@ export default function Header() { window.location.href = "/admin" }} className="fa-solid fa-lock">} { - window.localStorage.removeItem("userLogin") + window.localStorage.removeItem("token") window.location.href = "/auth" }} className="cursor-pointer fa-solid fa-right-from-bracket">
diff --git a/src/stores/slices/user.slice.ts b/src/stores/slices/user.slice.ts index b8ebcfc..ad5992d 100644 --- a/src/stores/slices/user.slice.ts +++ b/src/stores/slices/user.slice.ts @@ -21,8 +21,16 @@ const userSlice = createSlice({ }, extraReducers: (bd) => { + bd.addCase(fetchUserData.pending, (state, action) => { + state.loading = true + }) bd.addCase(fetchUserData.fulfilled, (state, action) => { - state.data = action.payload + console.log("đã vào full", action.payload) + state.loading = false + state.data = action.payload + }) + bd.addCase(fetchUserData.rejected, (state, action) => { + state.loading = false }) } }) @@ -31,8 +39,8 @@ const userSlice = createSlice({ const fetchUserData = createAsyncThunk( "user/fetchUserData", async () => { - let result = await Apis.user.findById(localStorage.getItem("userLogin")) - return result.data + let result = await Apis.user.me(localStorage.getItem("token")) as any + return result } )