import { readdir } from "fs/promises"; import express from "express"; import cors from "cors"; import helmet from "helmet"; import path from "path"; import fs from "fs/promises"; import { createServer } from "http"; import { Server } from "socket.io"; import morgan from "morgan"; import { STATIC_DIR } from "./constants.js"; import { getStructure, cache } from "./helper.js"; if (typeof process.env.API_TOKEN === "undefined") { throw new Error("Service cannot be started without API_TOKEN"); } // Load cache on start getStructure({ refresh: true }); const app = express(); const server = createServer(app); // Global boolean array let booleanArray = new Array(10).fill(false); // Socket.IO setup with CORS const io = new Server(server, { cors: { origin: "http://localhost:5173", // React app URL methods: ["GET", "POST"], }, }); const corsOptions = { methods: ["GET", "POST", "PUT", "DELETE"], credentials: true, origin: (origin, callback) => { if (!origin) { if (process.env.NODE_ENV === "production") { return callback(new Error("Origin required in production")); } return callback(null, true); } if (isOriginAllowed(origin)) { return callback(null, true); } else { return callback(new Error("Not allowed by CORS")); } }, }; app.use(cors(corsOptions)); app.use(helmet()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use(morgan("tiny")); // Socket connection handling //io.on("connection", (socket) => { // console.log("Client connected:", socket.id); // // // Send current array state to newly connected client // socket.emit("arrayChanged", booleanArray); // // // Handle array updates from client // socket.on("setArrayValue", (data) => { // const { index, value } = data; // // if (index >= 0 && index < booleanArray.length) { // booleanArray[index] = value; // console.log(`Updated index ${index} to ${value}`); // // // Broadcast updated array to all clients // io.emit("arrayChanged", booleanArray); // } // }); // // // Handle getting current array state // socket.on("getArray", () => { // socket.emit("arrayChanged", booleanArray); // }); // // socket.on("disconnect", () => { // console.log("Client disconnected:", socket.id); // }); //}); app.get("/_health", (req, res) => { res.json({ healthy: true }); }); app.get( "/resources-status", asyncHandler((req, res) => { res.json({ array: booleanArray }); }), ); app.get( "/structure", asyncHandler(async (req, res) => { let { refresh } = req.query; refresh = Boolean(refresh); const structure = await getStructure({ refresh }); res.json(structure); }), ); app.post( "/resources", verifyToken, asyncHandler(async (req, res) => { try { const { topicId, content } = req.body; if (!topicId || !content) { throw new Error("Missing topic id or content"); } const [subjectIdx, topicIdx] = topicId .match(/\d+/g) ?.map((n) => parseInt(n) - 1); // Get next resource version const subjects = await getStructure(); const resources = subjects[subjectIdx].topics[topicIdx].resources; const resourceVersion = resources.length + 1; const rv = `RV${resourceVersion}`; const resourceId = `F_${topicId}_${rv}`; const resource = { id: resourceId, filename: `${resourceId}.md`, version: resourceVersion, }; const filePath = path.join(STATIC_DIR, resource.filename); await fs.writeFile(filePath, content, "utf8"); subjects[subjectIdx].topics[topicIdx].resources.push(resource); res.json({ success: true, filename: resource.filename }); } catch (error) { console.log(error); res.status(500).json({ error: "Failed to update topic resource" }); } }), ); app.delete( "/resources/:filename", verifyToken, asyncHandler(async (req, res) => { try { const { filename } = req.params; if (!filename) { return res.status(400).json({ error: "Filename is required" }); } // Validate filename format (basic security check) const pattern = /^F_S\d+_T\d+_RV\d+\.md$/; if (!pattern.test(filename)) { return res.status(400).json({ error: "Invalid filename format" }); } const filePath = path.join(STATIC_DIR, filename); // Check if file exists try { await fs.access(filePath); } catch (error) { return res.status(404).json({ error: "File not found" }); } // Delete the file await fs.unlink(filePath); // Refresh the structure cache const structure = await getStructure({ refresh: true }); res.json({ success: true, message: `File ${filename} deleted successfully`, structure, }); } catch (error) { console.log(error); res.status(500).json({ error: "Failed to delete file" }); } }), ); app.use(errorRequestHandler); const PORT = process.env.PORT || 3000; server.listen(PORT, () => console.log("Resource Provider started")); /** * HELPERS */ function asyncHandler(fn) { return (req, res, next) => { return Promise.resolve(fn(req, res, next)).catch(next); }; } function errorRequestHandler(error, _req, res, next) { if (error) { console.log(error); res .status(error.status || 500) .json({ message: error.message || "Server failed" }); } else { next(); } } function isOriginAllowed(origin) { const url = new URL(origin); if (url.hostname === "localhost" || url.hostname === "127.0.0.1") { return true; } return url.hostname.endsWith("tomastm.com"); } function verifyToken(req, res, next) { const token = req.headers["token"]; if (!token || token !== process.env.API_TOKEN) { res.status(401).json({ message: "Token not provided" }); return; } next(); }