Compare commits

...

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_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", "@tailwindcss/vite": "^4.1.13",
"antd": "^5.27.4", "antd": "^5.27.4",
"axios": "^1.12.2", "axios": "^1.12.2",
"jose": "^6.1.0",
"json-server": "^1.0.0-beta.3", "json-server": "^1.0.0-beta.3",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
@@ -4157,6 +4158,15 @@
"jiti": "lib/jiti-cli.mjs" "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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@@ -16,6 +16,7 @@
"@tailwindcss/vite": "^4.1.13", "@tailwindcss/vite": "^4.1.13",
"antd": "^5.27.4", "antd": "^5.27.4",
"axios": "^1.12.2", "axios": "^1.12.2",
"jose": "^6.1.0",
"json-server": "^1.0.0-beta.3", "json-server": "^1.0.0-beta.3",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",

View File

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

View File

@@ -2,6 +2,7 @@ import axios from "axios"
import type { User } from "../../types/user.type" import type { User } from "../../types/user.type"
import { message } from "antd" import { message } from "antd"
import { ApiUtil } from "../../utils/api.util" import { ApiUtil } from "../../utils/api.util"
import * as jose from 'jose'
export interface UserSignInDTO { export interface UserSignInDTO {
emailOrUserName: string emailOrUserName: string
@@ -16,10 +17,7 @@ export interface UserFindAllDTO {
} }
export const UserApi = { export const UserApi = {
signIn: async (data: UserSignInDTO): Promise<{ signIn: async (data: UserSignInDTO) => {
message: string
data: any
}> => {
let userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?email=${data.emailOrUserName}`) let userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?email=${data.emailOrUserName}`)
if (userData.data.length == 0) { if (userData.data.length == 0) {
userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?userName=${data.emailOrUserName}`) userData = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?userName=${data.emailOrUserName}`)
@@ -36,10 +34,7 @@ export const UserApi = {
data: null data: null
}) })
} }
return { return createToken(userData.data[0].id)
message: "Đăng nhập thành công!",
data: userData.data[0] as User
}
} }
}, },
signUp: async (data: User) => { signUp: async (data: User) => {
@@ -106,5 +101,59 @@ export const UserApi = {
findAll: async (query?: UserFindAllDTO) => { findAll: async (query?: UserFindAllDTO) => {
let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?` + ApiUtil.writeQuery(query)) let result = await axios.get(`${import.meta.env.VITE_SV_HOST}/users?` + ApiUtil.writeQuery(query))
return result.data 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 return store.user
}) })
/* Đã đăng nhập và là master hoặc admin */
if (userStore.data?.role == UserRole.MASTER || userStore.data?.role == UserRole.ADMIN) { if (userStore.data?.role == UserRole.MASTER || userStore.data?.role == UserRole.ADMIN) {
return ( return (
<> <>
@@ -23,7 +23,8 @@ export default function ProtectedAdmin(
) )
} }
if (!userStore.data?.role) { /* chưa đăng nhập */
if (!userStore.data && !userStore.loading) {
return ( 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="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"> <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) { if (userStore.data?.role) {
window.location.href="/" window.location.href = "/"
return <></> return <></>
} }
/* chưa vào case, default */
async function signInHandle(e: FormEvent) { async function signInHandle(e: FormEvent) {
e.preventDefault() e.preventDefault()
let data: UserSignInDTO = { let data: UserSignInDTO = {
@@ -103,10 +109,9 @@ export default function ProtectedAdmin(
} }
try { try {
let result = await Apis.user.signIn(data) let result = await Apis.user.signIn(data)
localStorage.setItem("userLogin", JSON.stringify(result.data)) localStorage.setItem("token", result)
Modal.confirm({ Modal.confirm({
title: "Đăng nhập thành công", title: "Đăng nhập thành công",
content: result.message,
onOk: () => { onOk: () => {
window.location.reload() window.location.reload()
}, },

View File

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

View File

@@ -49,9 +49,9 @@ export default function Auth() {
console.log("loginData", loginData) console.log("loginData", loginData)
try { try {
let data = await Apis.user.signIn(loginData) let data = await Apis.user.signIn(loginData)
localStorage.setItem("userLogin", data.data.id) localStorage.setItem("token", data)
Modal.confirm({ 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: ``, content: ``,
onOk: () => { onOk: () => {
window.location.href = "/" window.location.href = "/"

View File

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

View File

@@ -21,8 +21,16 @@ const userSlice = createSlice({
}, },
extraReducers: (bd) => { extraReducers: (bd) => {
bd.addCase(fetchUserData.pending, (state, action) => {
state.loading = true
})
bd.addCase(fetchUserData.fulfilled, (state, action) => { 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( const fetchUserData = createAsyncThunk(
"user/fetchUserData", "user/fetchUserData",
async () => { async () => {
let result = await Apis.user.findById(localStorage.getItem("userLogin")) let result = await Apis.user.me(localStorage.getItem("token")) as any
return result.data return result
} }
) )