Files
ipwhitelist-app/research/prompt-opus-queries.md
T

6.2 KiB

Промпт для 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 (пишется ли при ошибках?)

Ограничения:

  • Не предлагай менять стек
  • Только конкретные строки с исправлениями
  • ВЕСЬ ОТВЕТ ОДНИМ БЛОКОМ — без свёрток, без интерактивных элементов, без ссылок. Чистый текст.
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,
};