148 lines
5.9 KiB
TypeScript
148 lines
5.9 KiB
TypeScript
import express from "express";
|
|
import http from "node:http";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import fs from "node:fs";
|
|
import cors from "cors";
|
|
import cookieParser from "cookie-parser";
|
|
import { Server as SocketIOServer } from "socket.io";
|
|
import { createExpressMiddleware } from "@trpc/server/adapters/express";
|
|
import { appRouter } from "../routers.js";
|
|
import { createContext } from "./context.js";
|
|
import { authMiddleware } from "../auth.js";
|
|
import { getDb } from "../db.js";
|
|
import { startAutoAbsentJob, stopAutoAbsentJob } from "../services/autoAbsent.js";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = path.resolve(__dirname, "..", "..");
|
|
const PORT = Number(process.env.PORT ?? 5000);
|
|
const NODE_ENV = process.env.NODE_ENV ?? "development";
|
|
const IS_PROD = NODE_ENV === "production";
|
|
|
|
async function bootstrap() {
|
|
// Eagerly initialize database connection (warns early if DATABASE_URL missing)
|
|
try {
|
|
await getDb();
|
|
// eslint-disable-next-line no-console
|
|
console.log("[db] connected");
|
|
} catch (err) {
|
|
// eslint-disable-next-line no-console
|
|
console.error("[db] connection failed:", err);
|
|
}
|
|
|
|
const app = express();
|
|
const httpServer = http.createServer(app);
|
|
|
|
// ── Socket.io ────────────────────────────────────────────────────────────
|
|
const io = new SocketIOServer(httpServer, {
|
|
cors: {
|
|
origin: IS_PROD ? false : true,
|
|
credentials: true,
|
|
},
|
|
path: "/socket.io",
|
|
});
|
|
(globalThis as unknown as { __socketIo: SocketIOServer }).__socketIo = io;
|
|
|
|
io.on("connection", (socket) => {
|
|
socket.on("clinic:subscribe", (clinicId: number) => {
|
|
if (typeof clinicId === "number") socket.join(`clinic:${clinicId}`);
|
|
});
|
|
socket.on("display:subscribe", (clinicId: number) => {
|
|
if (typeof clinicId === "number") socket.join(`display:${clinicId}`);
|
|
});
|
|
socket.on("patient:subscribe", (patientToken: string) => {
|
|
if (typeof patientToken === "string" && patientToken.length > 0) {
|
|
socket.join(`patient:${patientToken}`);
|
|
}
|
|
});
|
|
socket.on("clinic:unsubscribe", (clinicId: number) => {
|
|
if (typeof clinicId === "number") socket.leave(`clinic:${clinicId}`);
|
|
});
|
|
socket.on("display:unsubscribe", (clinicId: number) => {
|
|
if (typeof clinicId === "number") socket.leave(`display:${clinicId}`);
|
|
});
|
|
socket.on("patient:unsubscribe", (patientToken: string) => {
|
|
if (typeof patientToken === "string") socket.leave(`patient:${patientToken}`);
|
|
});
|
|
});
|
|
|
|
// ── Middlewares ──────────────────────────────────────────────────────────
|
|
app.use(
|
|
cors({
|
|
origin: IS_PROD ? false : ["http://localhost:5173", "http://127.0.0.1:5173"],
|
|
credentials: true,
|
|
})
|
|
);
|
|
app.use(express.json({ limit: "1mb" }));
|
|
app.use(cookieParser());
|
|
app.use(authMiddleware);
|
|
|
|
// ── Health check ─────────────────────────────────────────────────────────
|
|
app.get("/api/health", (_req, res) => {
|
|
res.json({ status: "ok", env: NODE_ENV, ts: Date.now() });
|
|
});
|
|
|
|
// ── tRPC ─────────────────────────────────────────────────────────────────
|
|
app.use(
|
|
"/api/trpc",
|
|
createExpressMiddleware({
|
|
router: appRouter,
|
|
createContext,
|
|
onError({ error, path }) {
|
|
if (error.code === "INTERNAL_SERVER_ERROR") {
|
|
// eslint-disable-next-line no-console
|
|
console.error(`[trpc] ${path}:`, error);
|
|
}
|
|
},
|
|
})
|
|
);
|
|
|
|
// ── Static client (production) ───────────────────────────────────────────
|
|
if (IS_PROD) {
|
|
const clientDist = path.resolve(ROOT, "dist", "client");
|
|
const indexHtml = path.resolve(clientDist, "index.html");
|
|
|
|
if (fs.existsSync(clientDist)) {
|
|
app.use(express.static(clientDist, { maxAge: "1h", index: false }));
|
|
app.get("*", (req, res, next) => {
|
|
if (req.path.startsWith("/api") || req.path.startsWith("/socket.io")) return next();
|
|
if (!fs.existsSync(indexHtml)) return next();
|
|
res.sendFile(indexHtml);
|
|
});
|
|
} else {
|
|
// eslint-disable-next-line no-console
|
|
console.warn(`[static] dist/client not found at ${clientDist}`);
|
|
}
|
|
}
|
|
|
|
// ── Error handler ────────────────────────────────────────────────────────
|
|
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
// eslint-disable-next-line no-console
|
|
console.error("[express] error:", err);
|
|
res.status(500).json({ error: "Internal Server Error" });
|
|
});
|
|
|
|
httpServer.listen(PORT, () => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[server] listening on http://0.0.0.0:${PORT} (${NODE_ENV})`);
|
|
// Démarre le job qui marque les patients absents après N minutes sans réponse
|
|
startAutoAbsentJob();
|
|
});
|
|
|
|
const shutdown = (signal: string) => {
|
|
// eslint-disable-next-line no-console
|
|
console.log(`[server] received ${signal}, shutting down`);
|
|
stopAutoAbsentJob();
|
|
io.close();
|
|
httpServer.close(() => process.exit(0));
|
|
setTimeout(() => process.exit(1), 10_000).unref();
|
|
};
|
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
}
|
|
|
|
bootstrap().catch((err) => {
|
|
// eslint-disable-next-line no-console
|
|
console.error("[server] failed to start:", err);
|
|
process.exit(1);
|
|
});
|