v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Some checks failed
test / test (push) Failing after 24s

Реализация целевого дизайна (docs/architecture/target-design.md):
- src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images)
- Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue)
- Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов
- Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history
- Даунскейл sharp до 1024px перед Codex + масштабирование области референса
- SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/)
- Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data
- Документация: README, docs/architecture/
This commit is contained in:
2026-08-27 12:52:27 +07:00
parent 1c36a21e0c
commit 401969cfdb
68 changed files with 9368 additions and 4085 deletions

146
src/http/app.js Normal file
View File

@@ -0,0 +1,146 @@
"use strict";
const path = require("path");
const express = require("express");
const multer = require("multer");
const { requestContext } = require("./middleware/request-context");
const { originCheck } = require("./middleware/origin-check");
const { createAuthMiddleware } = require("./middleware/auth");
const { RateLimiter } = require("./middleware/rate-limit");
const { errorHandler } = require("./middleware/error-handler");
const { createHealthRoutes } = require("./routes/health.routes");
const { createAuthRoutes } = require("./routes/auth.routes");
const { createHistoryRoutes } = require("./routes/history.routes");
const { createPreviewRoutes } = require("./routes/preview.routes");
const { createEditRoutes } = require("./routes/edit.routes");
const { createUpscaleRoutes } = require("./routes/upscale.routes");
const { createSuggestRoutes } = require("./routes/suggest.routes");
const { createJobsRoutes } = require("./routes/jobs.routes");
const { createEventsRoutes } = require("./routes/events.routes");
const { createResultRoutes } = require("./routes/result.routes");
const { isHeic } = require("../core/images/heic");
const { BadRequestError, NotFoundError } = require("../errors");
/**
* Создаёт экземпляр Express приложения с внедрением зависимостей (DI).
* @param {Object} deps
* @param {import('../config').loadConfig} deps.config
* @param {import('../logger').Logger} deps.logger
* @param {import('../core/auth/session-store').SessionStore} deps.sessionStore
* @param {string} deps.passwordHash
* @param {import('../core/history/history-service').HistoryService} deps.historyService
* @param {import('../core/jobs/job-service').JobService} deps.jobService
* @param {import('../core/comfy/comfy-client').ComfyClient} deps.comfyClient
* @param {import('../core/comfy/comfy-runtime').ComfyRuntime} deps.comfyRuntime
* @param {import('events').EventEmitter} deps.events
* @returns {express.Application}
*/
function createApp(deps) {
const {
config,
logger,
sessionStore,
passwordHash,
historyService,
jobService,
comfyClient,
comfyRuntime,
events,
} = deps;
const app = express();
app.disable("x-powered-by");
if (config.TRUST_PROXY) {
app.set("trust proxy", 1);
}
// Static files from public/
const publicDir = path.join(__dirname, "../../public");
app.use(
express.static(publicDir, {
maxAge: "1h",
setHeaders: (res, filePath) => {
if (filePath.endsWith("index.html")) {
res.setHeader("cache-control", "no-cache");
}
},
})
);
// Request context & body parsing
app.use(express.json({ limit: "16kb" }));
app.use(requestContext(logger));
app.use("/api", originCheck());
// Multer instance for uploads
const upload = multer({
storage: multer.memoryStorage(),
limits: {
fileSize: config.MAX_UPLOAD_BYTES,
files: 2,
fields: 8,
},
fileFilter: (_request, file, callback) => {
const supported = new Set([
"image/jpeg",
"image/jpg",
"image/png",
"image/webp",
"image/heic",
"image/heif",
]);
if (supported.has(file.mimetype.toLowerCase()) || isHeic(file)) {
callback(null, true);
return;
}
callback(
new BadRequestError(
"Формат файла не поддерживается. Загрузите фотографию в формате JPEG, PNG, WebP или HEIC/HEIF."
)
);
},
});
// Auth & Rate Limiter
const authMiddleware = createAuthMiddleware(sessionStore);
const rateLimiter = new RateLimiter({
windowMs: config.LOGIN_RATE_LIMIT.windowMs,
max: config.LOGIN_RATE_LIMIT.max,
logger,
});
// 1. Public API routes
app.use("/api", createHealthRoutes({ comfyRuntime, jobService, historyService, config }));
app.use("/api", createAuthRoutes({ sessionStore, passwordHash, rateLimiter, config, logger }));
// 2. Protected API routes
app.use("/api", authMiddleware.requireAuth);
app.use("/api", createHistoryRoutes({ historyService, upload, logger }));
app.use("/api", createPreviewRoutes({ upload }));
app.use("/api", createEditRoutes({ jobService, historyService, upload, logger }));
app.use("/api", createUpscaleRoutes({ jobService, historyService, upload, logger }));
app.use("/api", createSuggestRoutes({ jobService, historyService, upload, logger }));
app.use("/api", createJobsRoutes({ jobService }));
app.use("/api", createEventsRoutes({ jobService, events, logger }));
app.use("/api", createResultRoutes({ comfyClient }));
// 404 handler for unmatched /api requests
app.use("/api", (_req, _res, next) => {
next(new NotFoundError("Эндпоинт не найден."));
});
// Global error handler
app.use(errorHandler(logger));
return app;
}
module.exports = {
createApp,
};