v0.5.17: owner_email в companies + универсальный парсер токенов + jwt.decode fallback
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ipwhitelist",
|
"name": "ipwhitelist",
|
||||||
"version": "0.5.14",
|
"version": "0.5.17",
|
||||||
"description": "IP WhiteList microservice for cloud provider",
|
"description": "IP WhiteList microservice for cloud provider",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS companies (
|
|||||||
client_id VARCHAR(64) UNIQUE NOT NULL,
|
client_id VARCHAR(64) UNIQUE NOT NULL,
|
||||||
name VARCHAR(255),
|
name VARCHAR(255),
|
||||||
custom_limit INTEGER DEFAULT NULL CHECK (custom_limit IS NULL OR custom_limit >= 0),
|
custom_limit INTEGER DEFAULT NULL CHECK (custom_limit IS NULL OR custom_limit >= 0),
|
||||||
|
owner_email TEXT DEFAULT NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function createEntriesRouter({ q }) {
|
|||||||
const effectiveName = effectiveClientId === req.user.clientId
|
const effectiveName = effectiveClientId === req.user.clientId
|
||||||
? req.user.companyName
|
? req.user.companyName
|
||||||
: effectiveClientId;
|
: effectiveClientId;
|
||||||
return q.getOrCreateCompany(effectiveClientId, effectiveName);
|
return q.getOrCreateCompany(effectiveClientId, effectiveName, req.user.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/v1/entries — список записей + лимит
|
// GET /api/v1/entries — список записей + лимит
|
||||||
|
|||||||
+49
-15
@@ -94,8 +94,8 @@ async function initAuth() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
isOidc,
|
isOidc,
|
||||||
middleware: createMiddleware(isOidc),
|
middleware: createMiddleware(isOidc, devMode),
|
||||||
bearerMiddleware: createBearerMiddleware(isOidc),
|
bearerMiddleware: createBearerMiddleware(isOidc, devMode),
|
||||||
// OIDC-хелперы (используются в src/routes/auth.js)
|
// OIDC-хелперы (используются в src/routes/auth.js)
|
||||||
buildAuthUrl,
|
buildAuthUrl,
|
||||||
exchangeCode,
|
exchangeCode,
|
||||||
@@ -111,7 +111,7 @@ async function initAuth() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Middleware — проверяет req.session.user или Bearer-токен ─────────────────
|
// ── Middleware — проверяет req.session.user или Bearer-токен ─────────────────
|
||||||
function createMiddleware(isOidc) {
|
function createMiddleware(isOidc, devMode) {
|
||||||
return function authMiddleware(req, res, next) {
|
return function authMiddleware(req, res, next) {
|
||||||
// 1. Сессия — основной источник пользователя (OIDC и mock)
|
// 1. Сессия — основной источник пользователя (OIDC и mock)
|
||||||
if (req.session && req.session.user) {
|
if (req.session && req.session.user) {
|
||||||
@@ -123,7 +123,7 @@ function createMiddleware(isOidc) {
|
|||||||
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
||||||
if (bearer) {
|
if (bearer) {
|
||||||
try {
|
try {
|
||||||
const payload = verifyAnyToken(bearer);
|
const payload = verifyAnyToken(bearer, devMode);
|
||||||
req.user = userFromPayload(payload);
|
req.user = userFromPayload(payload);
|
||||||
return next();
|
return next();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -246,7 +246,7 @@ function verifyOidcToken(token) {
|
|||||||
* Универсальная верификация: пробует mock, затем OIDC (JWKS).
|
* Универсальная верификация: пробует mock, затем OIDC (JWKS).
|
||||||
* Позволяет принимать и mock-токены (тесты, dev), и реальные токены от Keycloak.
|
* Позволяет принимать и mock-токены (тесты, dev), и реальные токены от Keycloak.
|
||||||
*/
|
*/
|
||||||
function verifyAnyToken(token) {
|
function verifyAnyToken(token, devMode) {
|
||||||
let lastErr = null;
|
let lastErr = null;
|
||||||
// 1. Mock (быстрый, локальный)
|
// 1. Mock (быстрый, локальный)
|
||||||
if (mockKeyPair) {
|
if (mockKeyPair) {
|
||||||
@@ -264,21 +264,55 @@ function verifyAnyToken(token) {
|
|||||||
lastErr = e;
|
lastErr = e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 3. DEV_MODE — просто декодировать без проверки подписи
|
||||||
|
if (devMode) {
|
||||||
|
console.log('[auth] DEV_MODE — декодирую токен без проверки подписи:', lastErr ? lastErr.message : 'no verifier');
|
||||||
|
const decoded = jwt.decode(token);
|
||||||
|
if (!decoded) throw new Error('Malformed JWT');
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
throw lastErr || new Error('Token verification failed: no verifier available');
|
throw lastErr || new Error('Token verification failed: no verifier available');
|
||||||
}
|
}
|
||||||
|
|
||||||
function userFromPayload(payload) {
|
function userFromPayload(payload) {
|
||||||
// clientid может быть "WZ01325" или "WZ01325, WZ54321" (ТЗ-плюс §6.1)
|
// ── Компании ──────────────────────────────────────────────────────────
|
||||||
const rawClientId = payload.ClientID || payload.client_id || payload.preferred_username || 'UNKNOWN';
|
let allClientIds = [];
|
||||||
const allClientIds = rawClientId.split(',').map(s => s.trim()).filter(Boolean);
|
|
||||||
|
// 1. auth-api: ClientID = "WZ03709" или "WZ03709, WZ54321"
|
||||||
|
const rawClientId = payload.ClientID || payload.client_id || '';
|
||||||
|
if (rawClientId) {
|
||||||
|
allClientIds = rawClientId.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Keycloak: claims = ["wz03709-shturval-...", "wz03709-...-test-3"]
|
||||||
|
if (!allClientIds.length && Array.isArray(payload.claims)) {
|
||||||
|
const wzRe = /wz(\d+)/i;
|
||||||
|
const seen = new Set();
|
||||||
|
for (const c of payload.claims) {
|
||||||
|
const m = String(c).match(wzRe);
|
||||||
|
if (m) {
|
||||||
|
const wz = 'WZ' + m[1];
|
||||||
|
if (!seen.has(wz)) { seen.add(wz); allClientIds.push(wz); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const activeClientId = allClientIds[0] || 'UNKNOWN';
|
const activeClientId = allClientIds[0] || 'UNKNOWN';
|
||||||
|
|
||||||
|
// ── Email ─────────────────────────────────────────────────────────────
|
||||||
|
const isEmail = s => typeof s === 'string' && s.includes('@');
|
||||||
|
const email = isEmail(payload.email) ? payload.email
|
||||||
|
: isEmail(payload.login) ? payload.login
|
||||||
|
: isEmail(payload.sub) ? payload.sub
|
||||||
|
: isEmail(payload.preferred_username) ? payload.preferred_username
|
||||||
|
: payload.email || payload.login || payload.sub || 'unknown';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
email: payload.email || payload.login || 'unknown',
|
email,
|
||||||
clientId: activeClientId, // активная компания (первая в списке)
|
clientId: activeClientId,
|
||||||
allClientIds, // все компании пользователя (массив)
|
allClientIds,
|
||||||
activeClientId, // дубль для ясности
|
activeClientId,
|
||||||
companyId: payload.company_id || payload.sub || null,
|
companyId: payload.company_id || null,
|
||||||
companyName: payload.company_name || activeClientId,
|
companyName: payload.company_name || activeClientId,
|
||||||
isAdmin: activeClientId === ADMIN_CLIENT_ID,
|
isAdmin: activeClientId === ADMIN_CLIENT_ID,
|
||||||
};
|
};
|
||||||
@@ -287,14 +321,14 @@ function userFromPayload(payload) {
|
|||||||
// ── Bearer-only middleware (для /api/v1) ────────────────────────────────────
|
// ── Bearer-only middleware (для /api/v1) ────────────────────────────────────
|
||||||
// Не смотрит на сессию — всегда проверяет Bearer JWT явно.
|
// Не смотрит на сессию — всегда проверяет Bearer JWT явно.
|
||||||
// Используется в src/api/index.js вместо auth.middleware.
|
// Используется в src/api/index.js вместо auth.middleware.
|
||||||
function createBearerMiddleware(isOidc) {
|
function createBearerMiddleware(isOidc, devMode) {
|
||||||
return function bearerAuthMiddleware(req, res, next) {
|
return function bearerAuthMiddleware(req, res, next) {
|
||||||
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
|
||||||
if (!bearer) {
|
if (!bearer) {
|
||||||
return res.status(401).json({ error: 'Bearer token required' });
|
return res.status(401).json({ error: 'Bearer token required' });
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const payload = verifyAnyToken(bearer);
|
const payload = verifyAnyToken(bearer, devMode);
|
||||||
req.user = userFromPayload(payload);
|
req.user = userFromPayload(payload);
|
||||||
return next();
|
return next();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
+5
-4
@@ -3,13 +3,14 @@ const { validate, overlaps } = require('./validators');
|
|||||||
|
|
||||||
// ── Companies ──
|
// ── Companies ──
|
||||||
|
|
||||||
async function getOrCreateCompany(clientId, companyName) {
|
async function getOrCreateCompany(clientId, companyName, ownerEmail) {
|
||||||
// Атомарный upsert — исключает дубли компаний при параллельных запросах
|
// Атомарный upsert — исключает дубли компаний при параллельных запросах.
|
||||||
|
// ownerEmail сохраняется только при INSERT (не перезаписывается при UPDATE).
|
||||||
const res = await pool.query(
|
const res = await pool.query(
|
||||||
`INSERT INTO companies (client_id, name) VALUES ($1, $2)
|
`INSERT INTO companies (client_id, name, owner_email) VALUES ($1, $2, $3)
|
||||||
ON CONFLICT (client_id) DO UPDATE SET name = COALESCE(companies.name, EXCLUDED.name)
|
ON CONFLICT (client_id) DO UPDATE SET name = COALESCE(companies.name, EXCLUDED.name)
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[clientId, companyName || clientId]
|
[clientId, companyName || clientId, ownerEmail || null]
|
||||||
);
|
);
|
||||||
return res.rows[0];
|
return res.rows[0];
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -247,7 +247,7 @@
|
|||||||
<div class="lbl">компания</div>
|
<div class="lbl">компания</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<div class="num"><%= user.email %></div>
|
<div class="num"><%= isAdmin && selectedCompany ? (selectedCompany.owner_email || user.email) : user.email %></div>
|
||||||
<div class="lbl">пользователь</div>
|
<div class="lbl">пользователь</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user