fix: X-Imp-* headers bridge UI→API impersonation + timezone GMT+4
This commit is contained in:
+16
@@ -445,6 +445,22 @@ function createBearerMiddleware(isOidc, devMode) {
|
|||||||
try {
|
try {
|
||||||
const payload = verifyAnyToken(bearer, devMode);
|
const payload = verifyAnyToken(bearer, devMode);
|
||||||
req.user = userFromPayload(payload);
|
req.user = userFromPayload(payload);
|
||||||
|
|
||||||
|
// Проброс контекста имперсонации из UI-слоя (X-Imp-* заголовки)
|
||||||
|
// Устанавливаются ui/api-client.js → api.impHeaders()
|
||||||
|
const impEmail = req.headers['x-imp-email'] || '';
|
||||||
|
const impOrig = req.headers['x-imp-originalemail'] || '';
|
||||||
|
const impClientId = req.headers['x-imp-clientid'] || '';
|
||||||
|
if (impEmail) {
|
||||||
|
req.user.email = impEmail;
|
||||||
|
req.user.originalUserEmail = impOrig;
|
||||||
|
req.user.isImpersonated = true;
|
||||||
|
if (impClientId) {
|
||||||
|
req.user.clientId = impClientId;
|
||||||
|
req.user.activeClientId = impClientId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return res.status(401).json({ error: 'Invalid token: ' + e.message });
|
return res.status(401).json({ error: 'Invalid token: ' + e.message });
|
||||||
|
|||||||
+21
-5
@@ -22,7 +22,7 @@ const API_BASE = process.env.API_BASE || 'http://localhost:' + (process.env.PORT
|
|||||||
* @param {object} [body] — JSON body (для POST/PATCH)
|
* @param {object} [body] — JSON body (для POST/PATCH)
|
||||||
* @returns {Promise<{status: number, data: any}>}
|
* @returns {Promise<{status: number, data: any}>}
|
||||||
*/
|
*/
|
||||||
function apiRequest(method, path, token, body) {
|
function apiRequest(method, path, token, body, impHeaders) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const url = new URL(API_BASE + path);
|
const url = new URL(API_BASE + path);
|
||||||
const lib = url.protocol === 'https:' ? https : http;
|
const lib = url.protocol === 'https:' ? https : http;
|
||||||
@@ -38,6 +38,12 @@ function apiRequest(method, path, token, body) {
|
|||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
// Проброс контекста имперсонации из UI в API-слой
|
||||||
|
if (impHeaders) {
|
||||||
|
if (impHeaders.impEmail) opts.headers['X-Imp-Email'] = impHeaders.impEmail;
|
||||||
|
if (impHeaders.impOriginalEmail) opts.headers['X-Imp-OriginalEmail'] = impHeaders.impOriginalEmail;
|
||||||
|
if (impHeaders.impClientId) opts.headers['X-Imp-ClientId'] = impHeaders.impClientId;
|
||||||
|
}
|
||||||
if (bodyStr) {
|
if (bodyStr) {
|
||||||
opts.headers['Content-Type'] = 'application/json';
|
opts.headers['Content-Type'] = 'application/json';
|
||||||
opts.headers['Content-Length'] = Buffer.byteLength(bodyStr);
|
opts.headers['Content-Length'] = Buffer.byteLength(bodyStr);
|
||||||
@@ -65,13 +71,23 @@ function apiRequest(method, path, token, body) {
|
|||||||
|
|
||||||
// Хелперы — один вызов на метод
|
// Хелперы — один вызов на метод
|
||||||
const api = {
|
const api = {
|
||||||
get: (path, token) => apiRequest('GET', path, token),
|
get: (path, token, imp) => apiRequest('GET', path, token, null, imp),
|
||||||
post: (path, token, body) => apiRequest('POST', path, token, body),
|
post: (path, token, body, imp) => apiRequest('POST', path, token, body, imp),
|
||||||
patch: (path, token, body) => apiRequest('PATCH', path, token, body),
|
patch: (path, token, body, imp) => apiRequest('PATCH', path, token, body, imp),
|
||||||
delete: (path, token) => apiRequest('DELETE', path, token),
|
delete: (path, token, imp) => apiRequest('DELETE', path, token, null, imp),
|
||||||
|
|
||||||
// Достать Bearer из сессии
|
// Достать Bearer из сессии
|
||||||
token: (req) => req.session && req.session.token,
|
token: (req) => req.session && req.session.token,
|
||||||
|
|
||||||
|
// Построить заголовки имперсонации из req.user (для проброса в API-слой)
|
||||||
|
impHeaders: (req) => {
|
||||||
|
if (!req.user || !req.user.isImpersonated) return null;
|
||||||
|
return {
|
||||||
|
impEmail: req.user.email,
|
||||||
|
impOriginalEmail: req.user.originalUserEmail,
|
||||||
|
impClientId: req.user.activeClientId || req.user.clientId,
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = api;
|
module.exports = api;
|
||||||
|
|||||||
+10
-6
@@ -38,6 +38,7 @@ function createRouter() {
|
|||||||
// GET / — список записей
|
// GET / — список записей
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
const token = api.token(req);
|
const token = api.token(req);
|
||||||
|
const imp = api.impHeaders(req);
|
||||||
|
|
||||||
// ── Переключатель компании через IAM ────────────────────────────────
|
// ── Переключатель компании через IAM ────────────────────────────────
|
||||||
const switchTo = (req.query.switchTo || '').trim();
|
const switchTo = (req.query.switchTo || '').trim();
|
||||||
@@ -78,7 +79,7 @@ function createRouter() {
|
|||||||
let entries = [], limit = 15, selectedCompany = null, companies = [];
|
let entries = [], limit = 15, selectedCompany = null, companies = [];
|
||||||
|
|
||||||
if (req.user.adminMode) {
|
if (req.user.adminMode) {
|
||||||
const cr = await api.get('/api/v1/companies', token);
|
const cr = await api.get('/api/v1/companies', token, imp);
|
||||||
companies = cr.data.companies || [];
|
companies = cr.data.companies || [];
|
||||||
|
|
||||||
const companyId = req.query.company ? parseInt(req.query.company, 10) : null;
|
const companyId = req.query.company ? parseInt(req.query.company, 10) : null;
|
||||||
@@ -91,7 +92,7 @@ function createRouter() {
|
|||||||
|
|
||||||
if (selectedCompany) {
|
if (selectedCompany) {
|
||||||
const includeDeleted = req.query.includeDeleted === 'true' ? '&includeDeleted=true' : '';
|
const includeDeleted = req.query.includeDeleted === 'true' ? '&includeDeleted=true' : '';
|
||||||
const er = await api.get('/api/v1/entries?company=' + selectedCompany.id + includeDeleted, token);
|
const er = await api.get('/api/v1/entries?company=' + selectedCompany.id + includeDeleted, token, imp);
|
||||||
entries = er.data.entries || [];
|
entries = er.data.entries || [];
|
||||||
limit = er.data.limit || 15;
|
limit = er.data.limit || 15;
|
||||||
}
|
}
|
||||||
@@ -101,7 +102,7 @@ function createRouter() {
|
|||||||
const clientIdParam = req.user.allClientIds && req.user.allClientIds.length > 1
|
const clientIdParam = req.user.allClientIds && req.user.allClientIds.length > 1
|
||||||
? '?client_id=' + encodeURIComponent(activeClientId)
|
? '?client_id=' + encodeURIComponent(activeClientId)
|
||||||
: '';
|
: '';
|
||||||
const er = await api.get('/api/v1/entries' + clientIdParam, token);
|
const er = await api.get('/api/v1/entries' + clientIdParam, token, imp);
|
||||||
entries = er.data.entries || [];
|
entries = er.data.entries || [];
|
||||||
limit = er.data.limit || 15;
|
limit = er.data.limit || 15;
|
||||||
}
|
}
|
||||||
@@ -150,11 +151,12 @@ function createRouter() {
|
|||||||
// POST /add — создать запись
|
// POST /add — создать запись
|
||||||
router.post('/add', async (req, res) => {
|
router.post('/add', async (req, res) => {
|
||||||
const token = api.token(req);
|
const token = api.token(req);
|
||||||
|
const imp = api.impHeaders(req);
|
||||||
const { value, comment } = req.body;
|
const { value, comment } = req.body;
|
||||||
const cq = companyQuery(req);
|
const cq = companyQuery(req);
|
||||||
const back = backUrl(req);
|
const back = backUrl(req);
|
||||||
try {
|
try {
|
||||||
const r = await api.post('/api/v1/entries' + cq, token, { value, comment });
|
const r = await api.post('/api/v1/entries' + cq, token, { value, comment }, imp);
|
||||||
if (r.status === 201) {
|
if (r.status === 201) {
|
||||||
const msg = r.data.wasNormalized
|
const msg = r.data.wasNormalized
|
||||||
? 'Запись добавлена (CIDR нормализован до ' + r.data.entry.value_cidr + ')'
|
? 'Запись добавлена (CIDR нормализован до ' + r.data.entry.value_cidr + ')'
|
||||||
@@ -175,11 +177,12 @@ function createRouter() {
|
|||||||
// POST /edit/:id — обновить запись
|
// POST /edit/:id — обновить запись
|
||||||
router.post('/edit/:id', async (req, res) => {
|
router.post('/edit/:id', async (req, res) => {
|
||||||
const token = api.token(req);
|
const token = api.token(req);
|
||||||
|
const imp = api.impHeaders(req);
|
||||||
const { value, comment } = req.body;
|
const { value, comment } = req.body;
|
||||||
const cq = companyQuery(req);
|
const cq = companyQuery(req);
|
||||||
const back = backUrl(req);
|
const back = backUrl(req);
|
||||||
try {
|
try {
|
||||||
const r = await api.patch('/api/v1/entries/' + req.params.id + cq, token, { value, comment });
|
const r = await api.patch('/api/v1/entries/' + req.params.id + cq, token, { value, comment }, imp);
|
||||||
if (r.status === 200) {
|
if (r.status === 200) {
|
||||||
return res.redirect(back + (back.includes('?') ? '&' : '?') + 'success=' + encodeURIComponent('Запись обновлена'));
|
return res.redirect(back + (back.includes('?') ? '&' : '?') + 'success=' + encodeURIComponent('Запись обновлена'));
|
||||||
}
|
}
|
||||||
@@ -197,10 +200,11 @@ function createRouter() {
|
|||||||
// POST /delete/:id — удалить запись
|
// POST /delete/:id — удалить запись
|
||||||
router.post('/delete/:id', async (req, res) => {
|
router.post('/delete/:id', async (req, res) => {
|
||||||
const token = api.token(req);
|
const token = api.token(req);
|
||||||
|
const imp = api.impHeaders(req);
|
||||||
const cq = companyQuery(req);
|
const cq = companyQuery(req);
|
||||||
const back = backUrl(req);
|
const back = backUrl(req);
|
||||||
try {
|
try {
|
||||||
const r = await api.delete('/api/v1/entries/' + req.params.id + cq, token);
|
const r = await api.delete('/api/v1/entries/' + req.params.id + cq, token, imp);
|
||||||
if (r.status === 204) {
|
if (r.status === 204) {
|
||||||
return res.redirect(back + (back.includes('?') ? '&' : '?') + 'success=' + encodeURIComponent('Запись удалена'));
|
return res.redirect(back + (back.includes('?') ? '&' : '?') + 'success=' + encodeURIComponent('Запись удалена'));
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -133,7 +133,7 @@
|
|||||||
<td style="white-space:nowrap;color:var(--muted);">
|
<td style="white-space:nowrap;color:var(--muted);">
|
||||||
<%= new Date(r.created_at).toLocaleString('ru', {
|
<%= new Date(r.created_at).toLocaleString('ru', {
|
||||||
day: '2-digit', month: '2-digit', year: 'numeric',
|
day: '2-digit', month: '2-digit', year: 'numeric',
|
||||||
hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Moscow'
|
hour: '2-digit', minute: '2-digit', timeZone: 'Etc/GMT-4'
|
||||||
}) %>
|
}) %>
|
||||||
</td>
|
</td>
|
||||||
<td><%= r.user_email %></td>
|
<td><%= r.user_email %></td>
|
||||||
|
|||||||
+2
-2
@@ -337,11 +337,11 @@
|
|||||||
<td><code><%= e.value_cidr %></code></td>
|
<td><code><%= e.value_cidr %></code></td>
|
||||||
<td><%= e.comment || '—' %></td>
|
<td><%= e.comment || '—' %></td>
|
||||||
<td><%= e.created_by %></td>
|
<td><%= e.created_by %></td>
|
||||||
<td><%= new Date(e.created_at).toLocaleDateString('ru', {day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit',timeZone:'Europe/Moscow'}) %></td>
|
<td><%= new Date(e.created_at).toLocaleDateString('ru', {day:'numeric',month:'short',year:'numeric',hour:'2-digit',minute:'2-digit',timeZone:'Etc/GMT-4'}) %></td>
|
||||||
<!-- Дата последнего редактирования (null если не менялась) -->
|
<!-- Дата последнего редактирования (null если не менялась) -->
|
||||||
<td style="color:var(--muted);font-size:.8rem;">
|
<td style="color:var(--muted);font-size:.8rem;">
|
||||||
<%= e.updated_at
|
<%= e.updated_at
|
||||||
? new Date(e.updated_at).toLocaleDateString('ru', {day:'numeric',month:'short',hour:'2-digit',minute:'2-digit',timeZone:'Europe/Moscow'})
|
? new Date(e.updated_at).toLocaleDateString('ru', {day:'numeric',month:'short',hour:'2-digit',minute:'2-digit',timeZone:'Etc/GMT-4'})
|
||||||
: '—' %>
|
: '—' %>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-center" style="white-space:nowrap;">
|
<td class="text-center" style="white-space:nowrap;">
|
||||||
|
|||||||
Reference in New Issue
Block a user