import { existsSync } from "node:fs"; import { join } from "node:path"; import cors from "cors"; import "dotenv/config"; import express from "express"; import { z } from "zod"; import { AzureImageService } from "./azure-service"; import { TemplateService } from "./template-service"; const queryLocation = z.object({ location: z.string().min(1) }); const queryOffer = z.object({ location: z.string().min(1), publisher: z.string().min(1) }); const querySku = z.object({ location: z.string().min(1), publisher: z.string().min(1), offer: z.string().min(1) }); const queryVersion = z.object({ location: z.string().min(1), publisher: z.string().min(1), offer: z.string().min(1), sku: z.string().min(1) }); const renderBody = z.object({ templateFile: z.string().min(1), selection: z.object({ location: z.string().min(1), publisher: z.string().min(1), offer: z.string().min(1), sku: z.string().min(1), version: z.string().min(1) }) }); const makeApp = () => { const app = express(); const port = Number.parseInt(process.env.PORT ?? "3000", 10); const subscriptionId = process.env.AZURE_SUBSCRIPTION_ID; app.use(cors()); app.use(express.json()); if (!subscriptionId) { app.get("/api/health", (_req, res) => { res.status(500).json({ status: "error", message: "Missing AZURE_SUBSCRIPTION_ID" }); }); return { app, port }; } const azure = new AzureImageService(subscriptionId); const templates = new TemplateService(); app.get("/api/health", (_req, res) => { res.json({ status: "ok" }); }); app.get("/api/locations", async (_req, res, next) => { try { res.json(await azure.getLocations()); } catch (error) { next(error); } }); app.get("/api/publishers", async (req, res, next) => { try { const { location } = queryLocation.parse(req.query); res.json(await azure.getPublishers(location)); } catch (error) { next(error); } }); app.get("/api/offers", async (req, res, next) => { try { const { location, publisher } = queryOffer.parse(req.query); res.json(await azure.getOffers(location, publisher)); } catch (error) { next(error); } }); app.get("/api/skus", async (req, res, next) => { try { const { location, publisher, offer } = querySku.parse(req.query); res.json(await azure.getSkus(location, publisher, offer)); } catch (error) { next(error); } }); app.get("/api/versions", async (req, res, next) => { try { const { location, publisher, offer, sku } = queryVersion.parse(req.query); res.json(await azure.getVersions(location, publisher, offer, sku)); } catch (error) { next(error); } }); app.get("/api/templates", (_req, res) => { res.json(templates.getTemplates()); }); app.post("/api/render", (req, res, next) => { try { const payload = renderBody.parse(req.body); const rendered = templates.render(payload.templateFile, payload.selection); res.json({ rendered }); } catch (error) { next(error); } }); app.get("/api/sku-export", async (req, res, next) => { try { const { location, publisher, offer } = querySku.parse(req.query); const skus = await azure.getSkus(location, publisher, offer); res.json({ rendered: templates.buildSkuExport(skus) }); } catch (error) { next(error); } }); app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => { if (err instanceof z.ZodError) { res.status(400).json({ message: "Invalid request", issues: err.issues }); return; } const message = err instanceof Error ? err.message : "Unexpected error"; res.status(500).json({ message }); }); const frontendRoot = join(process.cwd(), "dist/frontend"); if (existsSync(frontendRoot)) { app.use(express.static(frontendRoot)); app.get(/^(?!\/api).*/, (_req, res) => { res.sendFile(join(frontendRoot, "index.html")); }); } return { app, port }; }; if (require.main === module) { const { app, port } = makeApp(); app.listen(port, () => { // eslint-disable-next-line no-console console.log(`azure-image-chooser listening on ${port}`); }); } export { makeApp };