# Промпт для Claude Opus 4 — гонки, транзакции, безопасность в queries.js ## Контекст (не анализируй) Node.js + Express + PostgreSQL (pg pool). Микросервис IP WhiteList. Клиенты создают до 15 доверенных IPv4/CIDR. Многоарендность (изоляция по company_id). Аудит всех изменений обязателен. ## Что нужно Ниже полный код `queries.js`. Найди: - Race conditions (TOCTOU между проверкой лимита и INSERT) - Отсутствие транзакций там где они нужны - SQL-инъекции - Ошибки изоляции (может ли пользователь компании А затронуть записи компании Б?) - Проблемы с audit_log (пишется ли при ошибках?) Ограничения: - Не предлагай менять стек - Только конкретные строки с исправлениями - ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов, без ссылок. Чистый текст. ```js const { pool } = require('./db'); const { validate, overlaps } = require('./validators'); // ── Companies ── async function getOrCreateCompany(clientId, companyName) { let res = await pool.query('SELECT * FROM companies WHERE client_id = $1', [clientId]); if (res.rows.length > 0) return res.rows[0]; res = await pool.query( 'INSERT INTO companies (client_id, name) VALUES ($1, $2) RETURNING *', [clientId, companyName || clientId] ); return res.rows[0]; } async function getLimit(company) { const defaultLimit = parseInt(process.env.DEFAULT_LIMIT, 10) || 15; return company.custom_limit || defaultLimit; } // ── Entries ── async function listEntries(companyId, includeDeleted = false) { let sql = 'SELECT * FROM whitelist_entries WHERE company_id = $1'; if (!includeDeleted) sql += ' AND deleted_at IS NULL'; sql += ' ORDER BY created_at DESC'; return (await pool.query(sql, [companyId])).rows; } async function createEntry(companyId, rawValue, comment, userEmail) { const { cidr, wasNormalized } = validate(rawValue); // Проверка лимита const company = (await pool.query('SELECT * FROM companies WHERE id = $1', [companyId])).rows[0]; const limit = await getLimit(company); const cnt = (await pool.query( 'SELECT COUNT(*)::int AS c FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL', [companyId] )).rows[0].c; if (cnt >= limit) throw new Error(`Лимит исчерпан: ${cnt} из ${limit}`); // Проверка дубликатов и пересечений const existing = (await pool.query( 'SELECT value_cidr FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL', [companyId] )).rows; for (const row of existing) { if (row.value_cidr === cidr) throw new Error('Такой адрес уже существует'); if (overlaps(cidr, row.value_cidr)) throw new Error(`Пересечение с существующей записью ${row.value_cidr}`); } const res = await pool.query( `INSERT INTO whitelist_entries (company_id, value_cidr, comment, created_by) VALUES ($1, $2, $3, $4) RETURNING *`, [companyId, cidr, comment || null, userEmail] ); // Аудит await logAudit(userEmail, companyId, 'CREATE', null, cidr, res.rows[0].id); return { entry: res.rows[0], wasNormalized }; } async function updateEntry(entryId, companyId, rawValue, comment, userEmail) { const old = (await pool.query( 'SELECT * FROM whitelist_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL', [entryId, companyId] )).rows[0]; if (!old) throw new Error('Запись не найдена'); const { cidr, wasNormalized } = validate(rawValue); const existing = (await pool.query( 'SELECT value_cidr FROM whitelist_entries WHERE company_id = $1 AND deleted_at IS NULL AND id <> $2', [companyId, entryId] )).rows; for (const row of existing) { if (row.value_cidr === cidr) throw new Error('Такой адрес уже существует'); if (overlaps(cidr, row.value_cidr)) throw new Error(`Пересечение с существующей записью ${row.value_cidr}`); } const res = await pool.query( `UPDATE whitelist_entries SET value_cidr = $1, comment = $2, updated_by = $3, updated_at = NOW() WHERE id = $4 AND company_id = $5 RETURNING *`, [cidr, comment || old.comment, userEmail, entryId, companyId] ); await logAudit(userEmail, companyId, 'UPDATE', old.value_cidr, cidr, entryId); return { entry: res.rows[0], wasNormalized }; } async function deleteEntry(entryId, companyId, userEmail) { const old = (await pool.query( 'SELECT * FROM whitelist_entries WHERE id = $1 AND company_id = $2 AND deleted_at IS NULL', [entryId, companyId] )).rows[0]; if (!old) throw new Error('Запись не найдена'); await pool.query( 'UPDATE whitelist_entries SET deleted_by = $1, deleted_at = NOW() WHERE id = $2', [userEmail, entryId] ); await logAudit(userEmail, companyId, 'DELETE', old.value_cidr, null, entryId); } // ── Export ── async function getExportCIDRs() { const rows = (await pool.query( 'SELECT value_cidr FROM whitelist_entries WHERE deleted_at IS NULL ORDER BY value_cidr' )).rows; return rows.map(r => r.value_cidr); } // ── Audit ── async function logAudit(userEmail, companyId, action, oldValue, newValue, entryId) { await pool.query( `INSERT INTO audit_log (user_email, company_id, action, old_value, new_value, entry_id) VALUES ($1, $2, $3, $4, $5, $6)`, [userEmail, companyId, action, oldValue, newValue, entryId || null] ); } async function getAudit(companyId = null) { let sql = 'SELECT * FROM audit_log'; const params = []; if (companyId) { sql += ' WHERE company_id = $1'; params.push(companyId); } sql += ' ORDER BY created_at DESC LIMIT 500'; return (await pool.query(sql, params)).rows; } module.exports = { getOrCreateCompany, getLimit, listEntries, createEntry, updateEntry, deleteEntry, getExportCIDRs, getAudit, }; ```