temp
This commit is contained in:
3
.env
3
.env
@@ -1 +1,2 @@
|
|||||||
VITE_SV_HOST="http://localhost:3000"
|
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",
|
"@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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,81 @@ 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 */
|
||||||
|
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) {
|
async function signInHandle(e: FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
let data: UserSignInDTO = {
|
let data: UserSignInDTO = {
|
||||||
@@ -103,10 +174,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()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user