diff --git a/package.json b/package.json index 29a6b5b..28504ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ipwhitelist", - "version": "0.5.14", + "version": "0.5.17", "description": "IP WhiteList microservice for cloud provider", "main": "server.js", "scripts": { diff --git a/sql/schema.sql b/sql/schema.sql index 2b8dc6b..9f3eeca 100644 --- a/sql/schema.sql +++ b/sql/schema.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS companies ( client_id VARCHAR(64) UNIQUE NOT NULL, name VARCHAR(255), 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(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); diff --git a/src/api/routes/entries.js b/src/api/routes/entries.js index c4537c2..e819d54 100644 --- a/src/api/routes/entries.js +++ b/src/api/routes/entries.js @@ -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 — список записей + лимит diff --git a/src/auth.js b/src/auth.js index 81ce63d..0e005ee 100644 --- a/src/auth.js +++ b/src/auth.js @@ -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) { diff --git a/src/queries.js b/src/queries.js index 7580d8a..8fe0a1e 100644 --- a/src/queries.js +++ b/src/queries.js @@ -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]; } diff --git a/views/index.ejs b/views/index.ejs index 48b4b0d..e763f46 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -247,7 +247,7 @@
компания
-
<%= user.email %>
+
<%= isAdmin && selectedCompany ? (selectedCompany.owner_email || user.email) : user.email %>
пользователь