const jwt = require('jsonwebtoken'); const db = require('../db/db'); const registry = require('../permissions/registry'); const { logDenied } = require('../utils/securityLog'); /* ── Default values for role_permissions — sourced from central registry ── */ const PERM_DEFAULTS = registry.buildDefaultsMap(); function authMiddleware(req, res, next) { const header = req.headers.authorization; if (!header || !header.startsWith('Bearer ')) { return res.status(401).json({ error: 'Unauthorized' }); } const token = header.slice(7); 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, 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. // If DB has token_version set, token MUST carry matching tv. // (payload.tv === undefined means old token without version — also revoke) 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, customRole: fresh.custom_role || null }; next(); } catch { res.status(401).json({ error: 'Token invalid or expired' }); } } /* Кастомные роли наследуют «базовые роли» (какие встроенные гейты проходят). Встроенные роли — быстрый путь без обращения к БД. */ const BUILTIN_ROLES = new Set(['admin', 'teacher', 'student', 'free_student']); let _roleBaseStmt = null; function effectiveRoles(role) { if (!role) return []; if (BUILTIN_ROLES.has(role)) return [role]; try { if (!_roleBaseStmt) _roleBaseStmt = db.prepare('SELECT base_roles FROM roles WHERE name = ?'); const row = _roleBaseStmt.get(role); if (row && row.base_roles) { const arr = JSON.parse(row.base_roles); if (Array.isArray(arr) && arr.length) return arr.indexOf(role) >= 0 ? arr : arr.concat(role); } } catch (_e) { /* таблицы roles может не быть на старом инстансе — деградация к самой роли */ } return [role]; } function requireRole(...roles) { return (req, res, next) => { 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' }); }; } /* ── Разрешено ли ОДНО право: user override → role override → дефолт реестра ── */ /* Разрешено ли ОДНО право: user override (без просрочки) → role_permissions[permRole] → role_permissions[baseRole] (фолбэк для кастомной роли) → дефолт реестра(baseRole). Для встроенной роли permRole === baseRole. */ function isEnabled(uid, permRole, baseRole, key) { const userRow = db.prepare( "SELECT enabled FROM user_permissions WHERE user_id = ? AND permission = ? AND (expires_at IS NULL OR expires_at > datetime('now'))" ).get(uid, key); if (userRow !== undefined) return userRow.enabled === 1; let roleRow = db.prepare('SELECT enabled FROM role_permissions WHERE role = ? AND permission = ?').get(permRole, key); if (roleRow === undefined && permRole !== baseRole) { roleRow = db.prepare('SELECT enabled FROM role_permissions WHERE role = ? AND permission = ?').get(baseRole, key); } return roleRow !== undefined ? roleRow.enabled === 1 : (PERM_DEFAULTS[baseRole]?.[key] ?? false); } /* ── Проверка права с учётом зависимостей (requires): own AND все requires ── */ function requirePermission(key) { return (req, res, next) => { if (req.user?.role === 'admin') return next(); const baseRole = req.user?.role; const uid = req.user?.id; if (!baseRole) return res.status(401).json({ error: 'Unauthorized' }); const permRole = req.user?.customRole || baseRole; // кастомная роль конфигурит права своим именем const reqs = (registry.PERMISSIONS[key] && registry.PERMISSIONS[key].requires) || []; const ok = isEnabled(uid, permRole, baseRole, key) && reqs.every(r => isEnabled(uid, permRole, baseRole, r)); if (ok) return next(); logDenied(req, 'perm_denied', key); return res.status(403).json({ error: 'Permission denied' }); }; } /* ── Parent link JWT auth (separate from user auth) ───────────────────── */ function parentAuth(req, res, next) { const header = req.headers.authorization; if (!header || !header.startsWith('Bearer ')) return res.status(401).json({ error: 'Unauthorized' }); const token = header.slice(7); try { const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); if (payload.type !== 'parent') return res.status(401).json({ error: 'Invalid token type' }); const link = db.prepare( 'SELECT id, student_id, is_active, expires_at FROM parent_links WHERE id = ?' ).get(payload.linkId); if (!link || !link.is_active) return res.status(401).json({ error: 'Link revoked' }); if (link.expires_at && new Date(link.expires_at) < new Date()) return res.status(401).json({ error: 'Link expired' }); req.parent = { linkId: link.id, studentId: link.student_id }; next(); } catch { res.status(401).json({ error: 'Token invalid or expired' }); } } /** * requirePermissionForStudents(key) — применяет проверку права ТОЛЬКО к ролям * ученика (student/free_student); учитель и админ проходят всегда. * Нужно для роутов, которыми пользуются и учителя, и ученики (ассистент, * материалы, игры, флеш-карты, exam-prep): ученическое право не должно ломать * доступ учителю (у учителя нет записи по ключу → isEnabled вернул бы false). */ function requirePermissionForStudents(key) { const guard = requirePermission(key); return (req, res, next) => { const r = req.user?.role; if (r === 'student' || r === 'free_student') return guard(req, res, next); return next(); }; } /* Alias: requireAuth = authMiddleware */ const requireAuth = authMiddleware; /** * perm(key) — ergonomic alias for requirePermission(key). * Throws at module-load time if `key` is not in the registry, * so typos are caught at startup rather than at runtime. */ function perm(key) { if (!registry.isKnown(key)) { throw new Error(`[auth] Unknown permission key: "${key}". Add it to backend/src/permissions/registry.js`); } return requirePermission(key); } /* optionalAuth: попытаться установить req.user, но не блокировать при отсутствии токена */ function optionalAuth(req, res, next) { const header = req.headers.authorization || ''; if (!header.startsWith('Bearer ')) return next(); const token = header.slice(7); try { const payload = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ['HS256'] }); 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, customRole: fresh.custom_role || null }; } } } catch {} next(); } module.exports = { authMiddleware, requireAuth, optionalAuth, requireRole, requirePermission, requirePermissionForStudents, perm, parentAuth, effectiveRoles };