feat: admin-роль (isAdmin, requireAdmin), getCompanyById, getAllCompanies, setLimit, getAudit с JOIN
This commit is contained in:
+28
-1
@@ -4,6 +4,10 @@ const crypto = require('crypto');
|
||||
const ISSUER = process.env.JWT_ISSUER || 'mock-auth-api';
|
||||
const JWKS_URL = process.env.JWKS_URL || '';
|
||||
|
||||
// clientId платформы, которому выдаётся роль администратора.
|
||||
// Задаётся через .env, по умолчанию WZ01112 (Nubes) согласно ТЗ.
|
||||
const ADMIN_CLIENT_ID = process.env.ADMIN_CLIENT_ID || 'WZ01112';
|
||||
|
||||
let keyPair = null;
|
||||
let jwks = null;
|
||||
|
||||
@@ -36,16 +40,22 @@ function initAuth() {
|
||||
|
||||
function createMiddleware() {
|
||||
return function authMiddleware(req, res, next) {
|
||||
// DEV_MODE: обход авторизации для локальной разработки.
|
||||
// Активен только если NODE_ENV != 'production' — в prod всегда требует токен.
|
||||
// DEV_ADMIN=true позволяет тестировать admin-функционал в dev-режиме.
|
||||
if (process.env.DEV_MODE === 'true' && process.env.NODE_ENV !== 'production') {
|
||||
req.user = {
|
||||
email: 'dev@test.local',
|
||||
clientId: 'WZ01325',
|
||||
companyId: '00000000-0000-0000-0000-000000000000',
|
||||
companyName: 'DEV',
|
||||
// DEV_ADMIN=true → имитировать admin-права в dev-окружении
|
||||
isAdmin: process.env.DEV_ADMIN === 'true',
|
||||
};
|
||||
return next();
|
||||
}
|
||||
|
||||
// Токен извлекаем из httpOnly-cookie (браузер) или Authorization header (API)
|
||||
var token = (req.cookies && req.cookies.jwt) ||
|
||||
(req.headers.authorization || '').replace('Bearer ', '');
|
||||
|
||||
@@ -63,6 +73,9 @@ function createMiddleware() {
|
||||
clientId: payload.ClientID,
|
||||
companyId: payload.company_id,
|
||||
companyName: payload.company_name || payload.ClientID,
|
||||
// Роль admin определяется по clientId платформы согласно ТЗ.
|
||||
// В проде: можно расширить проверкой отдельного claim из Keycloak.
|
||||
isAdmin: payload.ClientID === ADMIN_CLIENT_ID,
|
||||
};
|
||||
next();
|
||||
} catch (e) {
|
||||
@@ -75,8 +88,22 @@ function createMiddleware() {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Middleware-защита для admin-only маршрутов.
|
||||
* Использовать после app.use(auth.middleware):
|
||||
* app.get('/admin', requireAdmin, handler)
|
||||
*/
|
||||
function requireAdmin(req, res, next) {
|
||||
if (!req.user || !req.user.isAdmin) {
|
||||
return res.status(403).send('403 Forbidden: требуется роль администратора');
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
function verifyJWT(token) {
|
||||
if (JWKS_URL) {
|
||||
// TODO: реализовать верификацию через внешний JWKS-endpoint.
|
||||
// Нужен URL от команды Keycloak: JWKS_URL=https://auth-api.../jwks
|
||||
throw new Error('JWKS_URL verification not implemented yet');
|
||||
}
|
||||
return jwt.verify(token, keyPair.publicKey, {
|
||||
@@ -106,4 +133,4 @@ function issueJWT(claims) {
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = { initAuth };
|
||||
module.exports = { initAuth, requireAdmin };
|
||||
|
||||
+64
-3
@@ -141,6 +141,58 @@ async function deleteEntry(entryId, companyId, userEmail) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Admin: компании ──
|
||||
|
||||
/**
|
||||
* Получить одну компанию по числовому id (primary key).
|
||||
* Используется когда admin работает с чужой компанией:
|
||||
* нам не нужно getOrCreateCompany (которая привязывается к clientId пользователя).
|
||||
*
|
||||
* @param {number} id — companies.id
|
||||
* @returns {object|null} строка из таблицы companies или null
|
||||
*/
|
||||
async function getCompanyById(id) {
|
||||
const res = await pool.query('SELECT * FROM companies WHERE id = $1', [id]);
|
||||
return res.rows[0] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Получить все компании с количеством активных записей каждой.
|
||||
* Используется на странице администратора: список компаний + статистика.
|
||||
* LEFT JOIN гарантирует присутствие компаний с нулём записей.
|
||||
*
|
||||
* @returns {object[]} строки companies с доп. полем active_count
|
||||
*/
|
||||
async function getAllCompanies() {
|
||||
const res = await pool.query(`
|
||||
SELECT c.*,
|
||||
COUNT(e.id)::int AS active_count
|
||||
FROM companies c
|
||||
LEFT JOIN whitelist_entries e
|
||||
ON e.company_id = c.id AND e.deleted_at IS NULL
|
||||
GROUP BY c.id
|
||||
ORDER BY c.name, c.client_id
|
||||
`);
|
||||
return res.rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Задать индивидуальный лимит для компании.
|
||||
* newLimit = null → сброс на глобальный дефолт (DEFAULT_LIMIT из .env / 15).
|
||||
* newLimit = число → индивидуальный лимит, переопределяет глобальный.
|
||||
*
|
||||
* ТЗ 4.5: «снижение лимита не удаляет существующие записи, но блокирует создание новых».
|
||||
*
|
||||
* @param {number} companyId — companies.id
|
||||
* @param {number|null} newLimit — новый лимит или null для сброса
|
||||
*/
|
||||
async function setLimit(companyId, newLimit) {
|
||||
await pool.query(
|
||||
'UPDATE companies SET custom_limit = $1, updated_at = NOW() WHERE id = $2',
|
||||
[newLimit, companyId]
|
||||
);
|
||||
}
|
||||
|
||||
// ── Export ──
|
||||
|
||||
async function getExportCIDRs(companyId = null) {
|
||||
@@ -165,13 +217,20 @@ async function logAudit(userEmail, companyId, action, oldValue, newValue, entryI
|
||||
}
|
||||
|
||||
async function getAudit(companyId = null) {
|
||||
let sql = 'SELECT * FROM audit_log';
|
||||
// JOIN с companies чтобы выводить human-readable название компании в UI.
|
||||
// Лимит 500 строк — достаточно для оперативного мониторинга.
|
||||
// В проде можно добавить пагинацию.
|
||||
let sql = `
|
||||
SELECT a.*, c.name AS company_name, c.client_id
|
||||
FROM audit_log a
|
||||
JOIN companies c ON c.id = a.company_id
|
||||
`;
|
||||
const params = [];
|
||||
if (companyId) {
|
||||
sql += ' WHERE company_id = $1';
|
||||
sql += ' WHERE a.company_id = $1';
|
||||
params.push(companyId);
|
||||
}
|
||||
sql += ' ORDER BY created_at DESC LIMIT 500';
|
||||
sql += ' ORDER BY a.created_at DESC LIMIT 500';
|
||||
return (await pool.query(sql, params)).rows;
|
||||
}
|
||||
|
||||
@@ -180,4 +239,6 @@ module.exports = {
|
||||
listEntries, createEntry, updateEntry, deleteEntry,
|
||||
getExportCIDRs,
|
||||
getAudit,
|
||||
// admin
|
||||
getCompanyById, getAllCompanies, setLimit,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user