Skip to content
This repository was archived by the owner on Jul 28, 2026. It is now read-only.

Commit 7db82ec

Browse files
committed
feat: invalidate existing sessions on auth restrict via token_version
1 parent 4e692dd commit 7db82ec

4 files changed

Lines changed: 22 additions & 3 deletions

File tree

config/migrate.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const tables = {
1515
{ name: 'is_admin', def: 'TINYINT(1) NOT NULL DEFAULT 0' },
1616
{ name: 'restricted', def: 'TINYINT(1) NOT NULL DEFAULT 0' },
1717
{ name: 'auth_restricted', def: 'TINYINT(1) NOT NULL DEFAULT 0' },
18+
{ name: 'token_version', def: 'INT NOT NULL DEFAULT 0' },
1819
{ name: 'avatar', def: 'VARCHAR(255) DEFAULT NULL' },
1920
{ name: 'ptero_client_api_key', def: 'VARCHAR(255) DEFAULT NULL' },
2021
{ name: 'created_at', def: 'TIMESTAMP DEFAULT CURRENT_TIMESTAMP' },

middleware/auth.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import jwt from 'jsonwebtoken';
2+
import { query } from '../config/db.js';
23

34
const JWT_SECRET = process.env.JWT_SECRET;
45

@@ -8,7 +9,7 @@ if (!JWT_SECRET) {
89
console.error('JWT_SECRET contains unresolved shell expansion characters ($(), backticks). Generate a proper random key (e.g. openssl rand -hex 32) and hardcode it in .env');
910
}
1011

11-
export function authenticateToken(req, res, next) {
12+
export async function authenticateToken(req, res, next) {
1213
const authHeader = req.headers['authorization'];
1314
const token = authHeader && authHeader.split(' ')[1];
1415

@@ -19,6 +20,18 @@ export function authenticateToken(req, res, next) {
1920
try {
2021
const decoded = jwt.verify(token, JWT_SECRET);
2122
req.user = decoded;
23+
24+
if (decoded.tokenVersion !== undefined) {
25+
try {
26+
const rows = await query('SELECT token_version FROM users WHERE id = ?', [decoded.userId]);
27+
if (rows.length > 0 && rows[0].token_version !== decoded.tokenVersion) {
28+
return res.status(403).json({ error: 'Session expired. Please log in again.' });
29+
}
30+
} catch {
31+
return res.status(403).json({ error: 'Session validation failed.' });
32+
}
33+
}
34+
2235
next();
2336
} catch {
2437
return res.status(403).json({ error: 'Invalid or expired token' });

routes/admin.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ router.post('/login', async (req, res) => {
4949
}
5050

5151
const token = jwt.sign(
52-
{ userId: user.id, email: user.email, username: user.username, pteroId: user.ptero_user_id, isAdmin: true },
52+
{ userId: user.id, email: user.email, username: user.username, pteroId: user.ptero_user_id, isAdmin: true, tokenVersion: user.token_version },
5353
JWT_SECRET,
5454
{ expiresIn: '2h', algorithm: 'HS256' }
5555
);
@@ -348,7 +348,11 @@ router.post('/users/:id/toggle-auth-restriction', authenticateToken, requireAdmi
348348
}
349349

350350
const newStatus = users[0].auth_restricted ? 0 : 1;
351-
await query('UPDATE users SET auth_restricted = ? WHERE id = ?', [newStatus, userId]);
351+
if (newStatus) {
352+
await query('UPDATE users SET auth_restricted = 1, token_version = token_version + 1 WHERE id = ?', [userId]);
353+
} else {
354+
await query('UPDATE users SET auth_restricted = 0 WHERE id = ?', [userId]);
355+
}
352356

353357
await logActivity(req.user.userId, 'admin_toggle_auth_restriction', `${newStatus ? 'Auth restricted' : 'Auth unrestricted'} user #${userId}`);
354358
res.json({ success: true, auth_restricted: !!newStatus });

routes/auth.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ router.post('/login', async (req, res) => {
214214
username: user.username,
215215
pteroId: user.ptero_user_id,
216216
isAdmin: !!user.is_admin,
217+
tokenVersion: user.token_version,
217218
});
218219

219220
res.cookie('token', token, {

0 commit comments

Comments
 (0)