Initial local backup snapshot
This commit is contained in:
commit
acd9e14ba5
367 changed files with 118038 additions and 0 deletions
217
server/_core/index.ts
Normal file
217
server/_core/index.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import "dotenv/config";
|
||||
import express from "express";
|
||||
import { createServer } from "http";
|
||||
import net from "net";
|
||||
import path from "node:path";
|
||||
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
||||
import { appRouter, computeReservationAnalytics, runStatsReportScheduler } from "../routers";
|
||||
import { createContext } from "./context";
|
||||
import { serveStatic, setupVite } from "./vite";
|
||||
import { registerEmailActionRoutes } from "../emailActions";
|
||||
import { registerSocialAuthRoutes } from "../socialAuth";
|
||||
import { ENV } from "./env";
|
||||
import { runMaterialReturnScheduler } from "../materialReturnWorkflow";
|
||||
import { authenticateRequest, runUserPurgeScheduler } from "./auth";
|
||||
import { withEffectiveInternalAccess } from "../internalAccess";
|
||||
import { buildRuntimeHealth, ensureMaintenanceFlagDirectory, isMaintenanceModeEnabled, markRuntimeReady, markRuntimeShuttingDown } from "./runtime";
|
||||
|
||||
function isPortAvailable(port: number): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const server = net.createServer();
|
||||
server.listen(port, () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
server.on("error", () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
async function findAvailablePort(startPort: number = 3000): Promise<number> {
|
||||
for (let port = startPort; port < startPort + 20; port++) {
|
||||
if (await isPortAvailable(port)) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
throw new Error(`No available port found starting from ${startPort}`);
|
||||
}
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
const server = createServer(app);
|
||||
const schedulerTimers: NodeJS.Timeout[] = [];
|
||||
const openSockets = new Set<net.Socket>();
|
||||
let shutdownInFlight = false;
|
||||
|
||||
await ensureMaintenanceFlagDirectory();
|
||||
|
||||
server.on("connection", (socket) => {
|
||||
openSockets.add(socket);
|
||||
socket.on("close", () => {
|
||||
openSockets.delete(socket);
|
||||
});
|
||||
});
|
||||
|
||||
async function sendHealth(res: express.Response, mode: "live" | "ready") {
|
||||
const health = await buildRuntimeHealth();
|
||||
const status = mode === "live"
|
||||
? (health.checks.shuttingDown ? 503 : 200)
|
||||
: (health.ok ? 200 : 503);
|
||||
res.status(status).json(health);
|
||||
}
|
||||
|
||||
function renderMaintenancePage() {
|
||||
return `<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Maintenance en cours</title>
|
||||
<style>
|
||||
body{margin:0;font-family:Arial,sans-serif;background:#f6f9fc;color:#162133;display:grid;place-items:center;min-height:100vh;padding:24px}
|
||||
.panel{max-width:560px;background:#fff;border:1px solid #d5e2f3;border-radius:20px;padding:32px;box-shadow:0 12px 30px rgba(15,23,42,.08)}
|
||||
h1{margin:0 0 12px;font-size:32px;line-height:1.1}
|
||||
p{margin:0 0 10px;font-size:16px;line-height:1.6;color:#52627a}
|
||||
.badge{display:inline-flex;padding:6px 12px;border-radius:999px;background:#e8f2ff;color:#2c63c9;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;margin-bottom:18px}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main class="panel">
|
||||
<div class="badge">Maintenance</div>
|
||||
<h1>Le portail revient dans un instant</h1>
|
||||
<p>Nous appliquons une mise à jour de service pour garder une expérience stable, propre et fiable.</p>
|
||||
<p>Tu peux recharger la page dans quelques instants.</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
async function gracefulShutdown(signal: string) {
|
||||
if (shutdownInFlight) return;
|
||||
shutdownInFlight = true;
|
||||
console.log(`[Runtime] graceful shutdown triggered by ${signal}`);
|
||||
markRuntimeShuttingDown();
|
||||
schedulerTimers.forEach(clearTimeout);
|
||||
schedulerTimers.splice(0, schedulerTimers.length);
|
||||
|
||||
const forceCloseTimer = setTimeout(() => {
|
||||
for (const socket of Array.from(openSockets)) {
|
||||
socket.destroy();
|
||||
}
|
||||
}, ENV.gracefulShutdownTimeoutMs);
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.close(() => resolve());
|
||||
});
|
||||
|
||||
clearTimeout(forceCloseTimer);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.on("SIGTERM", () => {
|
||||
void gracefulShutdown("SIGTERM");
|
||||
});
|
||||
process.on("SIGINT", () => {
|
||||
void gracefulShutdown("SIGINT");
|
||||
});
|
||||
|
||||
// Configure body parser with larger size limit for file uploads
|
||||
app.use(express.json({ limit: "50mb" }));
|
||||
app.use(express.urlencoded({ limit: "50mb", extended: true }));
|
||||
app.use("/uploads", express.static(path.resolve(process.cwd(), "uploads")));
|
||||
app.get("/health/live", async (_req, res) => {
|
||||
await sendHealth(res, "live");
|
||||
});
|
||||
app.get("/health/ready", async (_req, res) => {
|
||||
await sendHealth(res, "ready");
|
||||
});
|
||||
app.get("/health", async (_req, res) => {
|
||||
await sendHealth(res, "ready");
|
||||
});
|
||||
// Email action routes for validate/refuse from email
|
||||
registerEmailActionRoutes(app);
|
||||
registerSocialAuthRoutes(app);
|
||||
app.get("/api/internal/stats/reservations", async (req, res) => {
|
||||
try {
|
||||
let authenticatedUser;
|
||||
try {
|
||||
authenticatedUser = await withEffectiveInternalAccess(await authenticateRequest(req));
|
||||
} catch {
|
||||
return res.status(401).json({ error: "Authentification requise" });
|
||||
}
|
||||
if (!authenticatedUser) {
|
||||
return res.status(401).json({ error: "Authentification requise" });
|
||||
}
|
||||
const allowedRoles = new Set(["accueil", "admin", "super_admin"]);
|
||||
if (!allowedRoles.has(authenticatedUser.role)) {
|
||||
return res.status(403).json({ error: "Accès réservé à l’accueil et aux administrateurs" });
|
||||
}
|
||||
|
||||
const rawPeriod = typeof req.query.period === "string" ? req.query.period : "month";
|
||||
const period = ["7d", "month", "quarter", "semester", "year"].includes(rawPeriod) ? rawPeriod as "7d" | "month" | "quarter" | "semester" | "year" : "month";
|
||||
const payload = await computeReservationAnalytics(period);
|
||||
return res.json(payload);
|
||||
} catch {
|
||||
return res.status(500).json({ error: "Impossible de récupérer les statistiques" });
|
||||
}
|
||||
});
|
||||
// tRPC API
|
||||
app.use(
|
||||
"/api/trpc",
|
||||
createExpressMiddleware({
|
||||
router: appRouter,
|
||||
createContext,
|
||||
})
|
||||
);
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
if (!["GET", "HEAD"].includes(req.method)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (req.path.startsWith("/api") || req.path.startsWith("/health") || req.path.startsWith("/uploads")) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
if (!(await isMaintenanceModeEnabled())) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
res.status(503).type("html").send(renderMaintenancePage());
|
||||
});
|
||||
|
||||
// development mode uses Vite, production mode uses static files
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
await setupVite(app, server);
|
||||
} else {
|
||||
serveStatic(app);
|
||||
}
|
||||
|
||||
const preferredPort = parseInt(process.env.PORT || "3000");
|
||||
const port = await findAvailablePort(preferredPort);
|
||||
|
||||
if (port !== preferredPort) {
|
||||
console.log(`Port ${preferredPort} is busy, using port ${port} instead`);
|
||||
}
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`Server running on http://localhost:${port}/`);
|
||||
|
||||
const baseUrl = (ENV.appBaseUrl || `http://localhost:${port}`).replace(/\/$/, "");
|
||||
const runScheduler = () => {
|
||||
runMaterialReturnScheduler(baseUrl).catch((error) => {
|
||||
console.error("[MaterialReturnScheduler] execution error:", error);
|
||||
});
|
||||
runStatsReportScheduler(baseUrl).catch((error) => {
|
||||
console.error("[StatsReportScheduler] execution error:", error);
|
||||
});
|
||||
runUserPurgeScheduler().catch((error) => {
|
||||
console.error("[UserPurgeScheduler] execution error:", error);
|
||||
});
|
||||
};
|
||||
|
||||
markRuntimeReady();
|
||||
schedulerTimers.push(setTimeout(runScheduler, 5_000));
|
||||
schedulerTimers.push(setInterval(runScheduler, 60 * 60 * 1000));
|
||||
});
|
||||
}
|
||||
|
||||
startServer().catch(console.error);
|
||||
Loading…
Add table
Add a link
Reference in a new issue