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

Commit efe832f

Browse files
committed
refactor: strict permission system with shared middleware
- Create middleware/permissions.js with permission constants and role definitions - Enhance middleware/auth.js: authenticateToken now validates user existence, auth_restricted from DB, supports cookies; add requireAdmin, requireNotRestricted, requireOwnership factory - Move requireAdmin to shared middleware, remove inline version from admin.js - Add restricted to JWT payload for all auth endpoints - Replace inline DB restriction/ownership checks with middleware on all server routes - Replace manual JWT verification with authenticateToken on activity and avatar routes - Add Host header validation middleware in production
1 parent dba6923 commit efe832f

6 files changed

Lines changed: 1746 additions & 1607 deletions

File tree

middleware/auth.js

Lines changed: 108 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,108 @@
1-
import jwt from 'jsonwebtoken';
2-
import { query } from '../config/db.js';
3-
4-
const JWT_SECRET = process.env.JWT_SECRET;
5-
6-
export async function authenticateToken(req, res, next) {
7-
const authHeader = req.headers['authorization'];
8-
const token = authHeader && authHeader.split(' ')[1];
9-
10-
if (!token) {
11-
return res.status(401).json({ error: 'Access token required' });
12-
}
13-
14-
try {
15-
const decoded = jwt.verify(token, JWT_SECRET);
16-
req.user = decoded;
17-
18-
if (decoded.tokenVersion !== undefined) {
19-
try {
20-
const rows = await query('SELECT token_version FROM users WHERE id = ?', [decoded.userId]);
21-
if (rows.length > 0 && rows[0].token_version !== decoded.tokenVersion) {
22-
return res.status(403).json({ error: 'Session expired. Please log in again.' });
23-
}
24-
} catch {
25-
return res.status(403).json({ error: 'Session validation failed.' });
26-
}
27-
}
28-
29-
next();
30-
} catch {
31-
return res.status(403).json({ error: 'Invalid or expired token' });
32-
}
33-
}
34-
35-
export function generateToken(payload) {
36-
return jwt.sign(payload, JWT_SECRET, {
37-
expiresIn: process.env.JWT_EXPIRES_IN || '2h',
38-
algorithm: 'HS256',
39-
});
40-
}
1+
import jwt from 'jsonwebtoken';
2+
import { query } from '../config/db.js';
3+
4+
const JWT_SECRET = process.env.JWT_SECRET;
5+
6+
async function getUserById(userId) {
7+
try {
8+
const rows = await query(
9+
'SELECT id, email, username, is_admin, restricted, auth_restricted, token_version, ptero_user_id FROM users WHERE id = ?',
10+
[userId]
11+
);
12+
return rows.length > 0 ? rows[0] : null;
13+
} catch {
14+
return null;
15+
}
16+
}
17+
18+
export async function authenticateToken(req, res, next) {
19+
let token = null;
20+
const authHeader = req.headers['authorization'];
21+
22+
if (authHeader && authHeader.startsWith('Bearer ')) {
23+
token = authHeader.split(' ')[1];
24+
} else if (req.cookies && req.cookies.token) {
25+
token = req.cookies.token;
26+
}
27+
28+
if (!token) {
29+
return res.status(401).json({ error: 'Access token required' });
30+
}
31+
32+
try {
33+
const decoded = jwt.verify(token, JWT_SECRET);
34+
const user = await getUserById(decoded.userId);
35+
36+
if (!user) {
37+
return res.status(403).json({ error: 'User no longer exists' });
38+
}
39+
40+
if (user.auth_restricted) {
41+
return res.status(403).json({ error: 'Your account has been restricted. Contact support for assistance.' });
42+
}
43+
44+
if (user.token_version !== decoded.tokenVersion) {
45+
return res.status(403).json({ error: 'Session expired. Please log in again.' });
46+
}
47+
48+
req.user = {
49+
userId: user.id,
50+
email: user.email,
51+
username: user.username,
52+
pteroId: user.ptero_user_id,
53+
isAdmin: !!user.is_admin,
54+
restricted: !!user.restricted,
55+
tokenVersion: user.token_version,
56+
};
57+
58+
next();
59+
} catch {
60+
return res.status(403).json({ error: 'Invalid or expired token' });
61+
}
62+
}
63+
64+
export function requireAdmin(req, res, next) {
65+
if (!req.user?.isAdmin) {
66+
return res.status(403).json({ error: 'Admin access required' });
67+
}
68+
next();
69+
}
70+
71+
export function requireNotRestricted(req, res, next) {
72+
if (req.user?.restricted) {
73+
return res.status(403).json({ error: 'Your account is restricted. This action is disabled.' });
74+
}
75+
next();
76+
}
77+
78+
export function requireOwnership(table, column, paramName, idSource = 'params') {
79+
return async (req, res, next) => {
80+
try {
81+
const id = parseInt(idSource === 'params' ? req.params[paramName] : req.body[paramName], 10);
82+
if (isNaN(id)) {
83+
return res.status(400).json({ error: 'Invalid ID' });
84+
}
85+
86+
const rows = await query(`SELECT user_id FROM ${table} WHERE ${column} = ?`, [id]);
87+
if (rows.length === 0) {
88+
return res.status(404).json({ error: 'Resource not found' });
89+
}
90+
91+
if (rows[0].user_id !== req.user.userId) {
92+
return res.status(403).json({ error: 'Access denied. Resource does not belong to you.' });
93+
}
94+
95+
next();
96+
} catch (err) {
97+
console.error('Ownership check error:', err.message);
98+
res.status(500).json({ error: 'Ownership verification failed' });
99+
}
100+
};
101+
}
102+
103+
export function generateToken(payload) {
104+
return jwt.sign(payload, JWT_SECRET, {
105+
expiresIn: process.env.JWT_EXPIRES_IN || '2h',
106+
algorithm: 'HS256',
107+
});
108+
}

middleware/permissions.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
export const ROLES = {
2+
USER: 'user',
3+
ADMIN: 'admin',
4+
};
5+
6+
export const PERMISSIONS = {
7+
// Server actions
8+
SERVER_CREATE: 'server:create',
9+
SERVER_RENEW: 'server:renew',
10+
SERVER_RENAME: 'server:rename',
11+
SERVER_REINSTALL: 'server:reinstall',
12+
SERVER_DELETE: 'server:delete',
13+
SERVER_POWER: 'server:power',
14+
SERVER_LIST: 'server:list',
15+
SERVER_DETAILS: 'server:details',
16+
SERVER_OVERVIEW: 'server:overview',
17+
SERVER_RESOURCES: 'server:resources',
18+
19+
// API key actions
20+
API_KEY_READ: 'api_key:read',
21+
API_KEY_WRITE: 'api_key:write',
22+
API_KEY_DELETE: 'api_key:delete',
23+
24+
// Account actions
25+
ACCOUNT_CHANGE_PASSWORD: 'account:change_password',
26+
ACCOUNT_CHANGE_EMAIL: 'account:change_email',
27+
ACCOUNT_DELETE: 'account:delete',
28+
ACCOUNT_EXPORT_DATA: 'account:export_data',
29+
ACCOUNT_UPLOAD_AVATAR: 'account:upload_avatar',
30+
ACCOUNT_VIEW_AVATAR: 'account:view_avatar',
31+
32+
// Notification actions
33+
NOTIFICATION_LIST: 'notification:list',
34+
NOTIFICATION_READ: 'notification:read',
35+
NOTIFICATION_READ_ALL: 'notification:read_all',
36+
37+
// Activity actions
38+
ACTIVITY_LIST: 'activity:list',
39+
40+
// Admin actions
41+
ADMIN_LOGIN: 'admin:login',
42+
ADMIN_CHECK: 'admin:check',
43+
ADMIN_LIST_SERVERS: 'admin:list_servers',
44+
ADMIN_VIEW_SERVER: 'admin:view_server',
45+
ADMIN_SUSPEND_SERVER: 'admin:suspend_server',
46+
ADMIN_UNSUSPEND_SERVER: 'admin:unsuspend_server',
47+
ADMIN_STOP_SERVER: 'admin:stop_server',
48+
ADMIN_FORCE_EXPIRE: 'admin:force_expire',
49+
ADMIN_DELETE_SERVER: 'admin:delete_server',
50+
ADMIN_LIST_USERS: 'admin:list_users',
51+
ADMIN_VIEW_USER: 'admin:view_user',
52+
ADMIN_TOGGLE_RESTRICTION: 'admin:toggle_restriction',
53+
ADMIN_TOGGLE_AUTH_RESTRICTION: 'admin:toggle_auth_restriction',
54+
ADMIN_TOGGLE_ADMIN: 'admin:toggle_admin',
55+
ADMIN_NOTIFY_USER: 'admin:notify_user',
56+
ADMIN_NOTIFY_ALL: 'admin:notify_all',
57+
ADMIN_DELETE_USER: 'admin:delete_user',
58+
ADMIN_VIEW_STATS: 'admin:view_stats',
59+
ADMIN_VIEW_ACTIVITY: 'admin:view_activity',
60+
ADMIN_MANAGE_NESTS: 'admin:manage_nests',
61+
ADMIN_MANAGE_EGGS: 'admin:manage_eggs',
62+
};
63+
64+
export const ROLE_PERMISSIONS = {
65+
[ROLES.USER]: [
66+
PERMISSIONS.SERVER_CREATE,
67+
PERMISSIONS.SERVER_RENEW,
68+
PERMISSIONS.SERVER_RENAME,
69+
PERMISSIONS.SERVER_REINSTALL,
70+
PERMISSIONS.SERVER_DELETE,
71+
PERMISSIONS.SERVER_POWER,
72+
PERMISSIONS.SERVER_LIST,
73+
PERMISSIONS.SERVER_DETAILS,
74+
PERMISSIONS.SERVER_OVERVIEW,
75+
PERMISSIONS.SERVER_RESOURCES,
76+
PERMISSIONS.API_KEY_READ,
77+
PERMISSIONS.API_KEY_WRITE,
78+
PERMISSIONS.API_KEY_DELETE,
79+
PERMISSIONS.ACCOUNT_CHANGE_PASSWORD,
80+
PERMISSIONS.ACCOUNT_CHANGE_EMAIL,
81+
PERMISSIONS.ACCOUNT_DELETE,
82+
PERMISSIONS.ACCOUNT_EXPORT_DATA,
83+
PERMISSIONS.ACCOUNT_UPLOAD_AVATAR,
84+
PERMISSIONS.ACCOUNT_VIEW_AVATAR,
85+
PERMISSIONS.NOTIFICATION_LIST,
86+
PERMISSIONS.NOTIFICATION_READ,
87+
PERMISSIONS.NOTIFICATION_READ_ALL,
88+
PERMISSIONS.ACTIVITY_LIST,
89+
],
90+
[ROLES.ADMIN]: [
91+
// Admin inherits all user permissions + admin-specific ones
92+
PERMISSIONS.ADMIN_LOGIN,
93+
PERMISSIONS.ADMIN_CHECK,
94+
PERMISSIONS.ADMIN_LIST_SERVERS,
95+
PERMISSIONS.ADMIN_VIEW_SERVER,
96+
PERMISSIONS.ADMIN_SUSPEND_SERVER,
97+
PERMISSIONS.ADMIN_UNSUSPEND_SERVER,
98+
PERMISSIONS.ADMIN_STOP_SERVER,
99+
PERMISSIONS.ADMIN_FORCE_EXPIRE,
100+
PERMISSIONS.ADMIN_DELETE_SERVER,
101+
PERMISSIONS.ADMIN_LIST_USERS,
102+
PERMISSIONS.ADMIN_VIEW_USER,
103+
PERMISSIONS.ADMIN_TOGGLE_RESTRICTION,
104+
PERMISSIONS.ADMIN_TOGGLE_AUTH_RESTRICTION,
105+
PERMISSIONS.ADMIN_TOGGLE_ADMIN,
106+
PERMISSIONS.ADMIN_NOTIFY_USER,
107+
PERMISSIONS.ADMIN_NOTIFY_ALL,
108+
PERMISSIONS.ADMIN_DELETE_USER,
109+
PERMISSIONS.ADMIN_VIEW_STATS,
110+
PERMISSIONS.ADMIN_VIEW_ACTIVITY,
111+
PERMISSIONS.ADMIN_MANAGE_NESTS,
112+
PERMISSIONS.ADMIN_MANAGE_EGGS,
113+
],
114+
};
115+
116+
export function hasPermission(user, permission) {
117+
if (!user) return false;
118+
if (user.isAdmin) return true;
119+
return ROLE_PERMISSIONS[ROLES.USER].includes(permission);
120+
}

0 commit comments

Comments
 (0)