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 { 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"); } 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")); } }, }; // Load cache on start getStructure({ refresh: true }); const app = express(); app.use(cors(corsOptions)); app.use(helmet()); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.get("/_health", (req, res) => { res.json({ healthy: true }); }); 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; app.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(); }