Files
azure-image-chooser/app-new/backend/src/server.ts

150 lines
4.2 KiB
TypeScript

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 };