fix: refresh token re-reads role from DB; remove staff stats cards
- /api/auth/refresh now fetches current role from DB instead of using stale Redis payload — role changes now take effect on next page refresh without requiring re-login. Also deactivates token if user.active=false. - Removed role-count stat cards from staff page (user request) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -291,11 +291,32 @@ const auth: FastifyPluginAsync = async (fastify) => {
|
|||||||
const stored = await redis.get(`refresh:${refreshToken}`)
|
const stored = await redis.get(`refresh:${refreshToken}`)
|
||||||
if (!stored) return reply.code(401).send({ error: 'Invalid or expired refresh token' })
|
if (!stored) return reply.code(401).send({ error: 'Invalid or expired refresh token' })
|
||||||
|
|
||||||
const payload = JSON.parse(stored) as JwtPayload
|
const cached = JSON.parse(stored) as JwtPayload
|
||||||
const accessToken = fastify.jwt.sign(payload, {
|
|
||||||
expiresIn: config.jwt.accessExpiry,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
// Re-read role and active status from DB so role changes take effect immediately
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`SELECT u.role, u.active, h.slug AS hotel_slug
|
||||||
|
FROM users u
|
||||||
|
LEFT JOIN hotels h ON h.id = u.hotel_id
|
||||||
|
WHERE u.id = $1`,
|
||||||
|
[cached.sub],
|
||||||
|
)
|
||||||
|
if (!rows[0] || !rows[0].active) {
|
||||||
|
await redis.del(`refresh:${refreshToken}`)
|
||||||
|
reply.clearCookie('refresh_token', { path: '/api/auth' })
|
||||||
|
return reply.code(401).send({ error: 'Account inactive or not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload: JwtPayload = {
|
||||||
|
...cached,
|
||||||
|
role: rows[0].role,
|
||||||
|
hotelSlug: rows[0].hotel_slug ?? cached.hotelSlug,
|
||||||
|
}
|
||||||
|
// Update Redis with fresh payload
|
||||||
|
const ttl = await redis.ttl(`refresh:${refreshToken}`)
|
||||||
|
if (ttl > 0) await redis.set(`refresh:${refreshToken}`, JSON.stringify(payload), 'EX', ttl)
|
||||||
|
|
||||||
|
const accessToken = fastify.jwt.sign(payload, { expiresIn: config.jwt.accessExpiry })
|
||||||
return { access_token: accessToken }
|
return { access_token: accessToken }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1025,10 +1025,8 @@ export function UsersPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = {
|
const totalUsers = users.length
|
||||||
total: users.length,
|
const activeUsers = users.filter(u => u.isActive).length
|
||||||
active: users.filter(u => u.isActive).length,
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 md:p-6 space-y-5">
|
<div className="p-4 md:p-6 space-y-5">
|
||||||
@@ -1036,7 +1034,7 @@ export function UsersPage() {
|
|||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Сотрудники</h1>
|
<h1 className="text-xl font-bold text-slate-900 dark:text-slate-100">Сотрудники</h1>
|
||||||
<p className="text-sm text-slate-500 dark:text-slate-400">{stats.total} пользователей · {stats.active} активных</p>
|
<p className="text-sm text-slate-500 dark:text-slate-400">{totalUsers} пользователей · {activeUsers} активных</p>
|
||||||
</div>
|
</div>
|
||||||
{tab === 'staff' && (
|
{tab === 'staff' && (
|
||||||
<button className="btn-primary" onClick={openCreate}>
|
<button className="btn-primary" onClick={openCreate}>
|
||||||
@@ -1066,25 +1064,6 @@ export function UsersPage() {
|
|||||||
|
|
||||||
{tab === 'staff' && (
|
{tab === 'staff' && (
|
||||||
<>
|
<>
|
||||||
{/* Stats */}
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
||||||
{(Object.entries(ROLE_META) as [StaffRole, typeof ROLE_META[StaffRole]][]).map(([role, meta]) => {
|
|
||||||
const count = users.filter(u => u.role === role).length
|
|
||||||
return (
|
|
||||||
<div key={role} className="card p-4 cursor-pointer hover:shadow-card-hover transition-shadow"
|
|
||||||
onClick={() => setRoleFilter(roleFilter === role ? 'all' : role)}>
|
|
||||||
<div className="flex items-center gap-2 mb-1">
|
|
||||||
<meta.icon size={14} className="text-slate-500 dark:text-slate-400" />
|
|
||||||
<span className={cn('text-xs font-medium px-1.5 py-0.5 rounded-full', meta.color)}>
|
|
||||||
{meta.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-2xl font-bold text-slate-900 dark:text-slate-100">{count}</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Filters */}
|
{/* Filters */}
|
||||||
<div className="flex gap-3 flex-wrap">
|
<div className="flex gap-3 flex-wrap">
|
||||||
<div className="relative flex-1 min-w-48">
|
<div className="relative flex-1 min-w-48">
|
||||||
|
|||||||
Reference in New Issue
Block a user