Files
maplibre-poc/test-backend/app.ts
2025-08-30 00:23:48 +02:00

133 lines
3.8 KiB
TypeScript

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