v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Some checks failed
test / test (push) Failing after 24s
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:
146
src/http/app.js
Normal file
146
src/http/app.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user