chore(ci+logs): eslint в CI, npm audit, logger с уровнями, retention error_logs

Шаги 1.6 и 1.7 плана production-готовности:
- eslint flat-config (баг-ловушки, легаси warn), lint блокирующий в pr-check
- npm audit --audit-level=high в CI (отчёт)
- фикс реального бага: условный useRef в TaskTitleInline
- server/utils/logger.ts (LOG_LEVEL/LOG_FORMAT) в 5 горячих местах
- error_logs retention 30 дней (worker), redactSensitive в captureErrorLog
- 0 errors lint, vitest 96/96
This commit is contained in:
2026-09-08 00:11:38 +03:00
parent 6eaca79cc6
commit abd5d4b65f
14 changed files with 1579 additions and 19 deletions

View File

@@ -8,6 +8,7 @@ import { notificationService } from "./services/notification.service";
import { webPushService } from "./services/web-push.service";
import { startMedScheduleWorker } from "./medschedule/worker";
import { startAutomationScheduler } from "./workers/automation-scheduler";
import { startErrorLogsRetention } from "./workers/error-logs-retention";
import { startGpsWorker } from "./gps/worker";
import { storage } from "./storage";
import type { ReminderRecipient } from "@shared/schema";
@@ -23,6 +24,10 @@ import { fileUploads, errorLogs } from "@shared/schema";
import { authenticateToken, authenticateFileToken, type AuthenticatedRequest } from "./middleware/auth.middleware";
import { EXT_TO_MIME, getFileExt } from "./utils/upload";
import crypto from "crypto";
import { logger } from "./utils/logger";
// Единый логгер для горячих мест index.ts (остальной файл исторически использует console/log из vite.ts)
const errLog = logger("express");
const app = express();
// Configure Express to trust proxy for accurate client IP identification
@@ -303,13 +308,24 @@ app.use((req, res, next) => {
const ERROR_LOG_EXCLUDED_PATHS = new Set(['/api/health', '/api/events']);
const errorLogDedupe = new Map<string, number>();
// Чувствительные данные не должны попадать в error_logs даже случайно:
// если тело ошибки содержит JSON с паролями/токенами — значения маскируем.
const SENSITIVE_KEY_RE = /("(?:password|token|access_token|refresh_token|secret|api_key|apiKey|authorization)"\s*:\s*")[^"]*(")/gi;
const BEARER_RE = /Bearer\s+[A-Za-z0-9\-._~+/=]+/gi;
function redactSensitive(text: string): string {
return text
.replace(SENSITIVE_KEY_RE, '$1***$2')
.replace(BEARER_RE, 'Bearer ***');
}
function captureErrorLog(req: Request, statusCode: number, path: string, body?: Record<string, any>) {
try {
if (statusCode < 400 || statusCode === 401) return;
if (ERROR_LOG_EXCLUDED_PATHS.has(path)) return;
const rawMessage = (body && (body.error || body.message)) || '';
const message = String(rawMessage).slice(0, 2000) || `HTTP ${statusCode}`;
const message = redactSensitive(String(rawMessage).slice(0, 2000)) || `HTTP ${statusCode}`;
const key = `${req.method} ${path} ${statusCode} ${message.slice(0, 200)}`;
const now = Date.now();
@@ -1077,13 +1093,13 @@ async function runStartupDataPatches() {
const status = err.status || err.statusCode || 500;
if (res.headersSent) {
console.error('[express] Error after headers sent:', err);
errLog.error('Error after headers sent:', err);
return;
}
if (status >= 500) {
// Клиенту — только нейтральный текст и id инцидента; детали (SQL, стек) — в серверный лог
const requestId = crypto.randomUUID().slice(0, 8);
console.error(`[express] Server error [${requestId}]:`, err);
errLog.error(`Server error [${requestId}]:`, err);
res.status(status).json({ message: `Внутренняя ошибка сервера (код ${requestId})` });
return;
}
@@ -1112,6 +1128,10 @@ async function runStartupDataPatches() {
startAutomationScheduler();
log('Automation scheduler started');
// Start error_logs retention (daily cleanup of API error log entries older than 30 days)
startErrorLogsRetention();
log('Error logs retention started');
// Start GPS worker (offline detection for tracked assets)
startGpsWorker();
log('GPS worker started');