Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 42 additions & 18 deletions backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import express from "express";
import { PrismaClient } from "@prisma/client";
import dotenv from "dotenv";

// Ensure top-level .env exists before loading
// Otherwise we'd deal with bunch of undefined env vars later
const envPath = path.resolve("./.env");
if (!fs.existsSync(envPath)) {
console.error("❌ Missing .env in project root!");
Expand All @@ -41,29 +39,27 @@ import { mountNotificationsRoutes } from "./routes/notifications.js";
const app = express();
const prisma = new PrismaClient();

// 🧩 Environment variables
const FRONTEND_ORIGIN = process.env.VITE_DEV_SERVER || "https://localhost:5173";
const BACKEND_ORIGIN = process.env.BACKEND_ORIGIN || "https://localhost:3000";
const ALLOWED_ORIGINS = (process.env.CORS_ALLOWED_ORIGINS || FRONTEND_ORIGIN)
.split(",")
.map((o) => o.trim());

// 🟢 Main startup
let server = null; // Need a reference for later shutdown

// Main startup
(async () => {
// 1️⃣ CORS setup
app.use(
cors({
origin: ALLOWED_ORIGINS,
credentials: true,
})
);

// 2️⃣ Core middleware
app.use(express.json());
app.use(cookieParser());
app.use(express.static("public"));

// 3️⃣ Sessions (secure + cross-origin)
app.set("trust proxy", 1);

const { default: FileStore } = await import("session-file-store");
Expand Down Expand Up @@ -91,15 +87,14 @@ const ALLOWED_ORIGINS = (process.env.CORS_ALLOWED_ORIGINS || FRONTEND_ORIGIN)
proxy: true,
cookie: {
httpOnly: true,
secure: true, // Always HTTPS
sameSite: "none", // ✅ Always allow cross-origin (5173 ↔ 3000, OIDC)
secure: true,
sameSite: "none",
domain: cookieDomain,
maxAge: 7 * 24 * 60 * 60 * 1000,
},
})
);

// 4️⃣ Mount all routes
await mountAuth(app, prisma);
mountStatsRoutes(app, prisma);
mountUserProfileRoutes(app, prisma);
Expand All @@ -112,7 +107,7 @@ const ALLOWED_ORIGINS = (process.env.CORS_ALLOWED_ORIGINS || FRONTEND_ORIGIN)
mountNowRoutes(app, prisma);
mountNotificationsRoutes(app, prisma);

// 🏠 Root endpoint (for quick inspection)
// Root endpoint (with API inspection)
app.get("/", (req, res) => {
const routes = [];

Expand Down Expand Up @@ -181,10 +176,8 @@ const ALLOWED_ORIGINS = (process.env.CORS_ALLOWED_ORIGINS || FRONTEND_ORIGIN)
`);
});

// 🩵 Health check
app.get("/api/health", (req, res) => res.json({ status: "ok" }));

// ⚙️ Slack OAuth redirect endpoint
app.get("/api/slack/oauth_redirect", (req, res) => {
res.send(`
<html>
Expand All @@ -196,12 +189,10 @@ const ALLOWED_ORIGINS = (process.env.CORS_ALLOWED_ORIGINS || FRONTEND_ORIGIN)
`);
});

// 🧩 Auth mode info
app.get("/api/auth-mode", (req, res) => {
res.json({ mode: process.env.AUTH_MODE || "LOCAL" });
});

// 🪪 Session debug
app.get("/api/debug/session", (req, res) => {
res.json({
hasSession: !!req.session,
Expand All @@ -210,7 +201,7 @@ const ALLOWED_ORIGINS = (process.env.CORS_ALLOWED_ORIGINS || FRONTEND_ORIGIN)
});
});

// 🔐 HTTPS startup
// HTTPS startup
const CERT_KEY = process.env.CERT_KEY_PATH || path.resolve("certs/localhost-key.pem");
const CERT_CRT = process.env.CERT_CRT_PATH || path.resolve("certs/localhost.pem");

Expand All @@ -220,14 +211,47 @@ const ALLOWED_ORIGINS = (process.env.CORS_ALLOWED_ORIGINS || FRONTEND_ORIGIN)
if (hasCerts) {
const key = fs.readFileSync(CERT_KEY);
const cert = fs.readFileSync(CERT_CRT);
https.createServer({ key, cert }, app).listen(port, () => {
server = https.createServer({ key, cert }, app).listen(port, () => {
console.log(`✅ HTTPS backend running at ${BACKEND_ORIGIN}`);
console.log(`🔒 Using certificates from: ${CERT_KEY} and ${CERT_CRT}`);
});
} else {
console.warn("⚠️ HTTPS certificates not found — falling back to HTTP.");
app.listen(port, () => {
server = app.listen(port, () => {
console.log(`⚙️ HTTP backend running at ${BACKEND_ORIGIN.replace("https", "http")}`);
});
}

// Graceful shutdown
async function gracefulShutdown(signal) {
console.log(`\n🛑 Received ${signal}. Shutting down gracefully...`);

if (server) {
server.close(async (err) => {
if (err) {
console.error("❌ Error closing server:", err);
}
try {
console.log("💾 Closing Prisma connection...");
await prisma.$disconnect();
console.log("✅ Database disconnected cleanly.");
} catch (e) {
console.error("⚠️ Error disconnecting database:", e);
}

console.log("👋 Bye!");
process.exit(0);
});

// Optional safety timeout (10s)
setTimeout(() => {
console.warn("⏱ Forced exit after timeout.");
process.exit(1);
}, 10000).unref();
}
Comment on lines +226 to +251
Copy link

Copilot AI Nov 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The graceful shutdown implementation has a potential issue: if server.close() callback is never called (e.g., if there are hanging connections), the process won't exit until the 10-second timeout.

Consider tracking active connections and closing them explicitly, or reducing the timeout duration to 5 seconds for faster feedback during development.

Copilot uses AI. Check for mistakes.
}

["SIGINT", "SIGTERM"].forEach((signal) => {
process.on(signal, () => gracefulShutdown(signal));
});
})();
4 changes: 4 additions & 0 deletions frontend/locale/cs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"meta": { "LanguageName": "Čeština" },
"cs": {}
}
Loading
Loading