import express, { Express } from "express" import cors from "cors" import path from "path" import fs from "fs" import { fileURLToPath } from "url" import { dirname } from "path" import { MetersCollection, MeterFeature } from "shared-types/meters" import { randomBytes } from "crypto" // ES module equivalent of __dirname const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) // Config const PORT = process.env.PORT || 3001 const FRONTEND_URL = process.env.FRONTEND_URL || "http://localhost:5173" // Create app factory so tests can import without starting the server function createApp(): Express { const app: Express = express() // Restrict CORS to the predefined frontend URL app.use( cors({ // origin: FRONTEND_URL, }) ) app.use(express.json()) // Require Origin header and reject mismatched origins app.use((req, res, next) => { const origin = req.headers.origin as string | undefined if (!origin) { return res.status(403).json({ error: "Missing Origin header" }) } // if (origin !== FRONTEND_URL) { // return res.status(403).json({ error: "Forbidden origin" }) // } next() }) // Per-request CSP nonce and header. Also expose nonce via X-CSP-Nonce so the frontend // can apply it to inline scripts/styles when needed. // app.use((req, res, next) => { // const nonce = randomBytes(16).toString("base64") // const csp = [ // `default-src 'self' ${FRONTEND_URL}`, // `connect-src 'self' ${FRONTEND_URL}`, // `img-src 'self' data: ${FRONTEND_URL}`, // `script-src 'self' 'nonce-${nonce}' ${FRONTEND_URL}`, // `style-src 'self' 'nonce-${nonce}' ${FRONTEND_URL}`, // ].join("; ") // res.setHeader("Content-Security-Policy", csp) // // Expose nonce so frontend templates can use it for inline scripts/styles // res.setHeader("X-CSP-Nonce", nonce) // // Vary on Origin so caches consider the Origin header when caching responses // res.setHeader("Vary", "Origin") // next() // }) return app } const app = createApp() // Route to serve the saudi_meters.json file app.get("/api/meters", (req, res) => { try { const filePath = path.join(__dirname, "saudi_meters.json") const data = fs.readFileSync(filePath, "utf8") const metersData: MetersCollection = JSON.parse(data) res.json(metersData) } catch (error) { console.error("Error reading meters data:", error) res.status(500).json({ error: "Failed to load meters data" }) } }) // Route to get individual meter by ID app.get("/api/meters/:id", (req, res) => { try { const { id } = req.params const filePath = path.join(__dirname, "saudi_meters.json") const data = fs.readFileSync(filePath, "utf8") const metersData: MetersCollection = JSON.parse(data) const meter = metersData.features.find( (feature: MeterFeature) => feature.properties.id === id ) if (meter) { res.json(meter) } else { res.status(404).json({ error: "Meter not found" }) } } catch (error) { console.error("Error reading meters data:", error) res.status(500).json({ error: "Failed to load meters data" }) } }) // Health check endpoint app.get("/health", (req, res) => { res.json({ status: "OK", timestamp: new Date().toISOString() }) }) // Basic info endpoint app.get("/", (req, res) => { res.json({ message: "Saudi Meters API Server", endpoints: { meters: "/api/meters", meterById: "/api/meters/:id", health: "/health", }, }) }) // Start the server only when run directly (not when imported by tests) if (process.argv[1] === fileURLToPath(import.meta.url)) { app.listen(PORT, () => { console.log(`🚀 Server running on http://localhost:${PORT}`) console.log( `📊 Meters data available at http://localhost:${PORT}/api/meters` ) }) } export default app