Initial local backup snapshot

This commit is contained in:
Selecta Keke 2026-06-24 00:02:55 -03:00
commit acd9e14ba5
367 changed files with 118038 additions and 0 deletions

217
server/_core/index.ts Normal file
View 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é à laccueil 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);