133 lines
3.8 KiB
TypeScript
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
|