This repository has been archived on 2026-10-08. You can view files and clone it, but cannot push or open issues or pull requests.

174 lines
4.8 KiB
JavaScript

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 {
SESSION_COOKIE,
bearerSessionMiddleware,
createOidcClient,
createOidcRouter,
} from "./auth/oidc.js";
import { createImagesRouter } from "./routes/images.js";
import { createHubRouter } from "./routes/hub.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,
}),
);
app.use("/api/hub", createHubRouter());
const sessionMiddleware = session({
name: SESSION_COOKIE,
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(bearerSessionMiddleware);
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(bearerSessionMiddleware);
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()})`,
);
});