security(auth): anti-enumeration, returnTo-валидация, лимит попыток логина

Шаг 2.1 плана production-готовности:
- единый 401 при любом отказе логина + constant-time bcrypt
- forgot-password: идентичный ответ независимо от существования email
- sanitizeReturnTo (open redirect fix в /api/documents/generate-link)
- 10 неудачных попыток → блок 15 мин (in-memory, аудит)
- DATABASE_SSL_REJECT_UNAUTHORIZED env (дефолт true)
- доделка 0.7: статичные тексты в llm-providers/rag/finance-di2
- 8 новых тестов (104/104)
This commit is contained in:
2026-09-08 00:46:57 +03:00
parent 0828003141
commit 5750a3106f
15 changed files with 525 additions and 45 deletions

View File

@@ -27,16 +27,20 @@ function getPool(): pg.Pool {
throw new Error(msg);
}
// SSL включается для sslmode=require и Neon. rejectUnauthorized управляется
// env DATABASE_SSL_REJECT_UNAUTHORIZED (дефолт true — проверка сертификата);
// 'false' допустимо только с самоподписанными сертификатами (dev-стенды).
const sslRequired =
process.env.DATABASE_URL.includes("sslmode=require") ||
process.env.DATABASE_URL.includes("neon.tech");
const sslRejectUnauthorized = process.env.DATABASE_SSL_REJECT_UNAUTHORIZED !== "false";
_pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 50,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
ssl:
process.env.DATABASE_URL.includes("sslmode=require") ||
process.env.DATABASE_URL.includes("neon.tech")
? { rejectUnauthorized: false }
: undefined,
ssl: sslRequired ? { rejectUnauthorized: sslRejectUnauthorized } : undefined,
});
_pool.on("error", (err) => {