v0.5.17: owner_email в companies + универсальный парсер токенов + jwt.decode fallback
This commit is contained in:
@@ -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
@@ -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
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user