v0.5.17: owner_email в companies + универсальный парсер токенов + jwt.decode fallback

This commit is contained in:
2026-06-03 15:17:56 +03:00
parent 0b3f1ab445
commit 878e46287b
6 changed files with 58 additions and 22 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ function createEntriesRouter({ q }) {
const effectiveName = effectiveClientId === req.user.clientId
? req.user.companyName
: effectiveClientId;
return q.getOrCreateCompany(effectiveClientId, effectiveName);
return q.getOrCreateCompany(effectiveClientId, effectiveName, req.user.email);
}
// GET /api/v1/entries — список записей + лимит
+49 -15
View File
@@ -94,8 +94,8 @@ async function initAuth() {
return {
isOidc,
middleware: createMiddleware(isOidc),
bearerMiddleware: createBearerMiddleware(isOidc),
middleware: createMiddleware(isOidc, devMode),
bearerMiddleware: createBearerMiddleware(isOidc, devMode),
// OIDC-хелперы (используются в src/routes/auth.js)
buildAuthUrl,
exchangeCode,
@@ -111,7 +111,7 @@ async function initAuth() {
}
// ── Middleware — проверяет req.session.user или Bearer-токен ─────────────────
function createMiddleware(isOidc) {
function createMiddleware(isOidc, devMode) {
return function authMiddleware(req, res, next) {
// 1. Сессия — основной источник пользователя (OIDC и mock)
if (req.session && req.session.user) {
@@ -123,7 +123,7 @@ function createMiddleware(isOidc) {
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
if (bearer) {
try {
const payload = verifyAnyToken(bearer);
const payload = verifyAnyToken(bearer, devMode);
req.user = userFromPayload(payload);
return next();
} catch (e) {
@@ -246,7 +246,7 @@ function verifyOidcToken(token) {
* Универсальная верификация: пробует mock, затем OIDC (JWKS).
* Позволяет принимать и mock-токены (тесты, dev), и реальные токены от Keycloak.
*/
function verifyAnyToken(token) {
function verifyAnyToken(token, devMode) {
let lastErr = null;
// 1. Mock (быстрый, локальный)
if (mockKeyPair) {
@@ -264,21 +264,55 @@ function verifyAnyToken(token) {
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');
}
function userFromPayload(payload) {
// clientid может быть "WZ01325" или "WZ01325, WZ54321" (ТЗ-плюс §6.1)
const rawClientId = payload.ClientID || payload.client_id || payload.preferred_username || 'UNKNOWN';
const allClientIds = rawClientId.split(',').map(s => s.trim()).filter(Boolean);
// ── Компании ──────────────────────────────────────────────────────────
let allClientIds = [];
// 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';
// ── 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 {
email: payload.email || payload.login || 'unknown',
clientId: activeClientId, // активная компания (первая в списке)
allClientIds, // все компании пользователя (массив)
activeClientId, // дубль для ясности
companyId: payload.company_id || payload.sub || null,
email,
clientId: activeClientId,
allClientIds,
activeClientId,
companyId: payload.company_id || null,
companyName: payload.company_name || activeClientId,
isAdmin: activeClientId === ADMIN_CLIENT_ID,
};
@@ -287,14 +321,14 @@ function userFromPayload(payload) {
// ── Bearer-only middleware (для /api/v1) ────────────────────────────────────
// Не смотрит на сессию — всегда проверяет Bearer JWT явно.
// Используется в src/api/index.js вместо auth.middleware.
function createBearerMiddleware(isOidc) {
function createBearerMiddleware(isOidc, devMode) {
return function bearerAuthMiddleware(req, res, next) {
const bearer = (req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
if (!bearer) {
return res.status(401).json({ error: 'Bearer token required' });
}
try {
const payload = verifyAnyToken(bearer);
const payload = verifyAnyToken(bearer, devMode);
req.user = userFromPayload(payload);
return next();
} catch (e) {
+5 -4
View File
@@ -3,13 +3,14 @@ const { validate, overlaps } = require('./validators');
// ── Companies ──
async function getOrCreateCompany(clientId, companyName) {
// Атомарный upsert — исключает дубли компаний при параллельных запросах
async function getOrCreateCompany(clientId, companyName, ownerEmail) {
// Атомарный upsert — исключает дубли компаний при параллельных запросах.
// ownerEmail сохраняется только при INSERT (не перезаписывается при UPDATE).
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)
RETURNING *`,
[clientId, companyName || clientId]
[clientId, companyName || clientId, ownerEmail || null]
);
return res.rows[0];
}