import config, { getEnvironment } from "./config.js"; import log4js from "log4js"; import express from "express"; import cors from "cors"; import path from "path"; import { fileURLToPath } from "url"; import { createServer } from "http"; import session from "express-session"; import MongoStore from "connect-mongo"; import { Server } from "socket.io"; import { connectMongo } from "./db/connect.js"; import { syncPatchesFromConfig } from "./services/patches.js"; import { recalculateAllPatchesGrowingState } from "./services/patchGrowingState.js"; import { syncDeviceFromConfig } from "./services/devices.js"; import { SocketManager } from "./socket/SocketManager.js"; import { MqttTtnIngest } from "./mqtt/MqttTtnIngest.js"; import { getWateringSessionsState } from "./services/allotmentState.js"; import { listWaterSchedulesForConfiguredDevice } from "./services/waterSchedules.js"; import { SubscriptionRegistry } from "./socket/SubscriptionRegistry.js"; import { registerAllotmentSocketApi } from "./socket/allotmentSocketApi.js"; import { startHistorySampler } from "./services/history.js"; import { createOidcClient, createOidcRouter } from "./auth/oidc.js"; import { createImagesRouter } from "./routes/images.js"; import { getLogger, getLogLevel } from "./logger.js"; const logger = getLogger("App"); const __dirname = path.dirname(fileURLToPath(import.meta.url)); const isProd = getEnvironment() === "production"; const clientUrl = config.app.urlClient; const mongoUri = config.database.mongo.url; await connectMongo(mongoUri); await syncDeviceFromConfig(config); await syncPatchesFromConfig(config); const PATCH_GROWING_RECALC_MS = 10 * 1000; try { await recalculateAllPatchesGrowingState(); } catch (e) { logger.error("Initial patch growing state recalc failed:", e); } try { await createOidcClient(); } catch (e) { logger.error("OIDC discovery/init failed:", e.message); } const app = express(); app.set("trust proxy", 1); app.use(log4js.connectLogger(logger, { level: "trace" })); app.use( cors({ origin: clientUrl, credentials: true, }), ); const sessionMiddleware = session({ name: "ta.sid", secret: config.auth.sessionSecret, resave: false, saveUninitialized: false, store: MongoStore.create({ mongoUrl: mongoUri, ttl: 14 * 24 * 60 * 60, }), cookie: { httpOnly: true, secure: isProd, sameSite: isProd ? "none" : "lax", maxAge: 14 * 24 * 60 * 60 * 1000, }, }); app.use(sessionMiddleware); app.use(express.json()); app.use(createOidcRouter(clientUrl)); app.use("/api/images", createImagesRouter()); app.get("/api/me", (req, res) => { if (req.session?.user) { res.json({ status: "ok", user: req.session.user }); } else { res.status(401).json({ status: "error", error: "Not authenticated" }); } }); const server = createServer(app); const io = new Server(server, { cors: { origin: clientUrl, methods: ["GET", "POST"], credentials: true, }, }); io.engine.use((req, res, next) => { sessionMiddleware(req, res, next); }); const subscriptionRegistry = new SubscriptionRegistry(); const socketManager = new SocketManager(io, { subscriptionRegistry }); async function notifyWateringSubscribers() { const data = await getWateringSessionsState(); socketManager.emitWateringSessionsUpdated(data); } async function notifyWaterSchedulesSubscribers() { const data = await listWaterSchedulesForConfiguredDevice(); socketManager.emitWaterSchedulesUpdated(data); } socketManager.registerSocketApi = (socket, client) => { registerAllotmentSocketApi(socket, { socketClient: client, subscriptionRegistry, notifyWateringSubscribers, notifyWaterSchedulesSubscribers, emitPatchUpdated: (patch) => socketManager.emitPatchUpdated(patch), }); }; const mqttIngest = new MqttTtnIngest(socketManager); mqttIngest.start(); startHistorySampler(); setInterval(async () => { try { const changed = await recalculateAllPatchesGrowingState(); for (const patch of changed) { socketManager.emitPatchUpdated(patch); } } catch (e) { logger.error("Periodic patch growing state recalc failed:", e); } }, PATCH_GROWING_RECALC_MS); if (isProd) { const staticPath = path.join(__dirname, "../client/dist"); app.use(express.static(staticPath)); app.get("*", (req, res, next) => { if (req.path.startsWith("/api") || req.path.startsWith("/auth")) { return next(); } res.sendFile(path.join(staticPath, "index.html")); }); } const PORT = config.server.port; server.listen(PORT, () => { logger.info( `Tom's Allotment server http://localhost:${PORT} (env=${getEnvironment()}, logLevel=${getLogLevel()})`, ); });