temp
This commit is contained in:
1
.env
1
.env
@@ -1 +1,2 @@
|
||||
VITE_SV_HOST="http://localhost:3000"
|
||||
VITE_JWT_TOKEN="phuocntbasdasasdasd"
|
||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,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 (
|
||||
<div className="w-full max-w-md mx-auto p-6 rounded-2xl bg-white/80 backdrop-blur-sm shadow-lg border border-gray-100">
|
||||
<div className="flex items-center gap-4">
|
||||
{/* avatar skeleton + spinner */}
|
||||
<div className="relative">
|
||||
<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="flex-1">
|
||||
<div className="h-4 bg-gray-200 rounded-md w-3/4 mb-3 animate-pulse" />
|
||||
<div className="h-3 bg-gray-200 rounded-md w-1/2 animate-pulse" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-6 grid grid-cols-3 gap-3">
|
||||
<div className="col-span-2">
|
||||
<div className="h-10 rounded-lg bg-gradient-to-r from-gray-100 via-gray-200 to-gray-100 animate-shimmer" />
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="h-10 w-10 rounded-lg border border-dashed border-gray-200 flex items-center justify-center">
|
||||
<svg className="h-5 w-5 animate-bounce" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden>
|
||||
<path d="M12 2v6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-indigo-500" />
|
||||
<path d="M12 22v-6" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-indigo-300" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="mt-4 text-center text-sm text-gray-500">
|
||||
Đang xác thực — kết nối tới máy chủ...
|
||||
</div>
|
||||
|
||||
|
||||
{/* visually-hidden for screen readers */}
|
||||
<span className="sr-only">Loading authentication information</span>
|
||||
|
||||
|
||||
<style>{`/* tiny shimmer animation without external libraries */
|
||||
@keyframes shimmer {
|
||||
0% { background-position: -250px 0 }
|
||||
100% { background-position: 250px 0 }
|
||||
}
|
||||
.animate-shimmer {
|
||||
background-size: 500px 100%;
|
||||
background-image: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.6) 50%, rgba(255,255,255,0) 100%);
|
||||
animation: shimmer 1.6s infinite;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</div>
|
||||
)
|
||||
|
||||
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()
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user