2 Commits

Author SHA1 Message Date
1a1fb7b67b temp 2 2025-10-08 07:44:27 +07:00
4df6ba7843 temp 2025-10-08 07:37:43 +07:00
11 changed files with 128 additions and 23 deletions

1
.env
View File

@@ -1 +1,2 @@
VITE_SV_HOST="http://localhost:3000"
VITE_JWT_TOKEN="phuocntbasdasasdasd"

10
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -1,9 +1,18 @@
import { createContext } from 'react'
import RouterSetup from './RouterSetup'
import { useSelector } from 'react-redux'
import type { StoreType } from './stores'
import Loading from './components/Loading'
export default function App() {
const userStore = useSelector((store: StoreType) => store.user)
return (
<RouterSetup />
<>
{
userStore.loading ? <Loading />
: <RouterSetup />
}
</>
)
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,22 @@
import React from 'react'
export default function Loading() {
return (
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-white/70 backdrop-blur-sm">
<div className="relative mb-4">
<div className="h-14 w-14 rounded-full bg-gray-200 animate-pulse" />
<svg
className="absolute -right-2 -bottom-2 h-7 w-7 animate-spin"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" className="text-gray-300 opacity-70" />
<path d="M22 12a10 10 0 00-10-10" stroke="currentColor" strokeWidth="3" className="text-indigo-500" strokeLinecap="round" />
</svg>
</div>
<div className="text-gray-600 font-medium">Đang tải dữ liệu xác thực...</div>
</div>
)
}

View File

@@ -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 (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-4">
<div className="w-full max-w-md bg-white rounded-2xl shadow-2xl overflow-hidden">
@@ -90,11 +91,16 @@ 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 */
async function signInHandle(e: FormEvent) {
e.preventDefault()
let data: UserSignInDTO = {
@@ -103,10 +109,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()
},

View File

@@ -28,7 +28,7 @@ export default function HeaderCom({ collapsed, setCollapsed }: { collapsed: bool
</select>
<Button onClick={() => {
localStorage.removeItem("userLogin")
localStorage.removeItem("token")
window.location.reload()
}}>logout</Button>
</Header>

View File

@@ -49,9 +49,9 @@ export default function Auth() {
console.log("loginData", loginData)
try {
let data = await Apis.user.signIn(loginData)
localStorage.setItem("userLogin", data.data.id)
localStorage.setItem("token", data)
Modal.confirm({
title: `Chào mừng ${data.data.userName} đã quay trở lại`,
title: `Chào mừng bạn đã quay trở lại`,
content: ``,
onOk: () => {
window.location.href = "/"

View File

@@ -70,7 +70,7 @@ export default function Header() {
window.location.href = "/admin"
}} className="fa-solid fa-lock"></i>}
<i onClick={() => {
window.localStorage.removeItem("userLogin")
window.localStorage.removeItem("token")
window.location.href = "/auth"
}} className="cursor-pointer fa-solid fa-right-from-bracket"></i>
</div>

View File

@@ -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
}
)