feat(permissions): C-2 — присвоение кастомной роли пользователю (users.custom_role)
Миграция 055: ADD COLUMN users.custom_role (безопасно, без пересборки users). Модель: users.role = функциональная база (встроенная, CHECK ок, драйвит ветки контроллеров и резолв прав), users.custom_role = имя кастомной роли. updateRole (PATCH /api/admin/users/:id/role) принимает кастомные роли → ставит base_roles[0] как базу + custom_role=имя; встроенная → custom_role=NULL; неизвестная → 400. authMiddleware/optionalAuth читают custom_role → req.user.customRole; requireRole расширяет до effectiveRoles(customRole||role). Тесты custom-roles 7/7; backend без регрессий. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,7 @@ function authMiddleware(req, res, next) {
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
|
||||
// Re-fetch role + token_version from DB so changes take effect immediately
|
||||
const fresh = db.prepare('SELECT role, token_version, is_banned FROM users WHERE id = ?').get(payload.id);
|
||||
const fresh = db.prepare('SELECT role, custom_role, token_version, is_banned FROM users WHERE id = ?').get(payload.id);
|
||||
if (!fresh) return res.status(401).json({ error: 'User not found' });
|
||||
if (fresh.is_banned) return res.status(403).json({ error: 'Аккаунт заблокирован' });
|
||||
// Invalidate tokens issued before password change / role change.
|
||||
@@ -25,7 +25,7 @@ function authMiddleware(req, res, next) {
|
||||
if (fresh.token_version != null && payload.tv !== fresh.token_version) {
|
||||
return res.status(401).json({ error: 'Token revoked — please log in again' });
|
||||
}
|
||||
req.user = { ...payload, role: fresh.role };
|
||||
req.user = { ...payload, role: fresh.role, customRole: fresh.custom_role || null };
|
||||
next();
|
||||
} catch {
|
||||
res.status(401).json({ error: 'Token invalid or expired' });
|
||||
@@ -52,7 +52,7 @@ function effectiveRoles(role) {
|
||||
|
||||
function requireRole(...roles) {
|
||||
return (req, res, next) => {
|
||||
const eff = effectiveRoles(req.user?.role);
|
||||
const eff = effectiveRoles(req.user?.customRole || req.user?.role);
|
||||
if (eff.some(r => roles.includes(r))) return next();
|
||||
if (req.user) logDenied(req, 'forbidden', `роль ${req.user.role}; требуется ${roles.join('/')}`);
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
@@ -137,10 +137,10 @@ function optionalAuth(req, res, next) {
|
||||
const token = header.slice(7);
|
||||
try {
|
||||
const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] });
|
||||
const fresh = db.prepare('SELECT role, token_version, is_banned FROM users WHERE id = ?').get(payload.id);
|
||||
const fresh = db.prepare('SELECT role, custom_role, token_version, is_banned FROM users WHERE id = ?').get(payload.id);
|
||||
if (fresh && !fresh.is_banned) {
|
||||
if (fresh.token_version == null || payload.tv === fresh.token_version) {
|
||||
req.user = { ...payload, role: fresh.role };
|
||||
req.user = { ...payload, role: fresh.role, customRole: fresh.custom_role || null };
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
Reference in New Issue
Block a user