import { ActivityPerformanceSchema, PlayerSchema, } from "@blade-and-brawn/domain" import { cors } from "@elysiajs/cors"; import { Elysia, NotFoundError, status, t } from "elysia"; import { PrintfulError, WebflowError, Printful, Webflow, } from "@blade-and-brawn/commerce"; import { DEFAULT_NAME, env, log } from "./util"; import serverTiming from "@elysia/server-timing"; import jwt from "@elysia/jwt"; import { CommerceService } from "./services/commerce"; import cluster from "node:cluster"; import { randomUUIDv7, sleep } from "bun"; import { CalculatorService, CalculatorUnavailableError } from "./services/calculator"; import { StandardsParamsSchema } from "@blade-and-brawn/calculator"; import { StandardsService } from "./services/standards"; import { EventsService, EventStatusSchema } from "./services/events"; // CONSTANTS // ----------------------- const EVENT_QUEUE_MANAGE_DELAY_MS = 1000 // 1 sec // SERVICES // ----------------------- const s = (() => { const Standards = new StandardsService(); const Calculator = new CalculatorService(DEFAULT_NAME); const Commerce = new CommerceService(); const Events = new EventsService(); return { Standards, Calculator, Commerce, Events }; })(); // QUEUES // ----------------------- const queues = [ s.Commerce.Apparel.Syncs.Queue, s.Commerce.Apparel.Orders.Queue, ] as const; // PLUGINS // ----------------------- const authPlugin = new Elysia({ name: "auth" }) .use(jwt({ name: "jwt", secret: env.AUTH_SECRET })) .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) }) .macro({ auth: { async resolve({ jwt, cookie: { auth } }) { const token = auth.value && await jwt.verify(auth.value); if (!token || !token.sessionId) throw status(401, "Unauthorized"); return { sessionId: token.sessionId.toString() }; } } }); // ELYSIA // ----------------------- export const app = new Elysia() .use(serverTiming()) .use( cors({ origin: [ // production /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.com$/i, // testing /^https?:\/\/([a-z0-9-]+\.)?bladeandbrawn\.webflow\.io$/i, // development "http://localhost:5173", ], }), ) .guard({ cookie: t.Cookie({ auth: t.Optional(t.String()) }) }) .use(authPlugin) .error({ PrintfulError, WebflowError, CalculatorUnavailableError }) .onError(({ code, error }) => { switch (code) { case "PrintfulError": case "WebflowError": log.error( { upstreamStatus: error.upstreamStatus, payload: error.payload }, error.message, ); return status(error.status, { error: error.message }); case "CalculatorUnavailableError": log.error({ err: error.cause }, error.message); return status(error.status, { error: error.message }); case "NOT_FOUND": return status(error.status, { error: error.message }); case "VALIDATION": return status(error.status, { error: error.message }); default: log.error({ err: error }, "unhandled error"); } }) .onAfterResponse(({ request, status, path }) => { if (env.NODE_ENV === "development") { const skip: Record = { "/commerce/products/sync/": ["GET"] }; if (skip[path]?.includes(request.method)) return; } log.info({ method: request.method, path, status }, "request"); }) .get("/", () => ({ status: "ok" })) .get("/health", () => ({ status: "ok" })) // AUTHENTICATION .post("/auth/login", async ({ jwt, body, cookie: { auth } }) => { if (body.password !== env.ADMIN_PASSWORD) throw status(401, "Invalid credentials"); auth.set({ value: await jwt.sign({ sessionId: randomUUIDv7(), exp: "7d" }), path: "/", maxAge: 60 * 60 * 24 * 7, sameSite: "lax", httpOnly: true, secure: env.NODE_ENV === "production", domain: env.NODE_ENV === "production" ? ".bladeandbrawn.com" : undefined }); }, { body: t.Object({ password: t.String() }) }) // CALCULATOR .group("/calculator", (app) => app // Non-authenticated .post("/calculate", async ({ body }) => { return { levels: await s.Calculator.calculate(body.player, body.activityPerformances) }; }, { body: t.Object({ player: PlayerSchema, activityPerformances: t.Array(ActivityPerformanceSchema) }), }) // Authenticated .guard({ auth: true }) .get("/standards/config", async () => { return await s.Calculator.Standards.Config.get(); }) .post("/standards/config/switch", async ({ body: { standardsConfigId } }) => { await s.Calculator.Standards.Config.switch(standardsConfigId); }, { body: t.Object({ standardsConfigId: t.String() }) }) ) .group("/standards", { auth: true }, (app) => app .post("/configs", async ({ body: { name, datasetId, params } }) => { return await s.Standards.Configs.create(name, datasetId, params); }, { body: t.Object({ name: t.String(), datasetId: t.String(), params: StandardsParamsSchema }) }) .get("/configs", async () => { return await s.Standards.Configs.list(); }) .get("/configs/:id", async ({ params: { id } }) => { return await s.Standards.Configs.get(id); }, { params: t.Object({ id: t.String() }) }) .put("/configs/:id", async ({ params: { id }, body: { name, datasetId, params: parameters } }) => { await s.Standards.Configs.update(id, name, datasetId, parameters); }, { params: t.Object({ id: t.String() }), body: t.Object({ name: t.String(), datasetId: t.String(), params: StandardsParamsSchema }) }) .delete("/configs/:id", async ({ params: { id } }) => { await s.Standards.Configs.delete(id); }, { params: t.Object({ id: t.String() }) }) .get("/datasets", async () => { return await s.Standards.Datasets.list(); }) .get("/datasets/:id", async ({ params: { id } }) => { return await s.Standards.Datasets.get(id); }, { params: t.Object({ id: t.String() }) }) .patch("/datasets/:id", async ({ params: { id }, body: { name } }) => { await s.Standards.Datasets.update(id, name); }, { params: t.Object({ id: t.String() }), body: t.Object({ name: t.String() }) }) ) // COMMERCE .group("/commerce", (app) => app .group("/products", { auth: true }, (app) => app .group("/sync", (app) => app // Sync status .get("/", async ({ sessionId }) => { const latestSyncState = await s.Commerce.Apparel.Syncs.getLatestSyncState(sessionId); if (!latestSyncState) throw new NotFoundError("No product sync found for the provided session"); return latestSyncState; }) // Run sync .post("/:pProductId?", async ({ params: { pProductId }, sessionId }) => { await s.Commerce.Apparel.Syncs.Queue.enqueue({ type: "apparel_sync_update", source: "portal", payload: { session: { id: sessionId, name: "Portal" }, filter: { pProductIds: pProductId ? [pProductId] : undefined } } }); }, { params: t.Object({ pProductId: t.Optional(t.Numeric()) }) }), ) .get("/:pProductId", async ({ params: { pProductId } }) => { const pProduct = await s.Commerce.Printful.Products.get(pProductId); if (!pProduct) throw new NotFoundError("Missing printful product"); const wProductId = pProduct.sync_product.external_id.split("-")[0]; if (!wProductId) throw new NotFoundError("Missing webflow product ID"); const wProduct = await s.Commerce.Webflow.Products.get(wProductId); return { pProduct, wProduct }; }, { params: t.Object({ pProductId: t.Numeric() }) }))) // EVENTS .group("/events", { auth: true }, (app) => app .get("/", async ({ query }) => { const events = await s.Events.list({ filter: { status: query.status, type: query.type, group: query.group }, limit: query.limit, offset: query.offset, }); return events.map((event) => ({ ...event, status: EventsService.status(event) })); }, { query: t.Object({ status: t.Optional(EventStatusSchema), type: t.Optional(t.String()), group: t.Optional(t.String()), limit: t.Optional(t.Numeric()), offset: t.Optional(t.Numeric()), }) }) .get("/:id", async ({ params: { id } }) => { const event = await s.Events.get(id); if (!event) throw new NotFoundError("Event not found"); return { ...event, status: EventsService.status(event) }; }, { params: t.Object({ id: t.String() }) }) .post("/:id/replay", async ({ params: { id } }) => { const replayed = await s.Events.replay(id); if (!replayed) throw new NotFoundError("Event not found or not in a failed state"); }, { params: t.Object({ id: t.String() }) }) ) // WEBHOOKS .post("/webhooks/printful", async ({ body }) => { // https://webflow.com/integrations/printful const payload = body as Printful.Webhook.EventPayload; switch (payload.type) { case Printful.Webhook.Event.ProductUpdated: { const pProduct = payload.data.sync_product; log.info({ productId: pProduct.id }, "printful webhook: product updated"); await s.Commerce.Apparel.Syncs.Queue.enqueue({ type: "apparel_sync_update", source: "printful", payload: { session: { id: randomUUIDv7(), name: "Printful" }, filter: { pProductIds: [pProduct.id] } } }); break; } case Printful.Webhook.Event.ProductDeleted: { const pProduct = payload.data.sync_product; const wProductId = pProduct.external_id.split("-")[0]; log.info({ externalId: payload.data.sync_product.external_id, wProductId }, "printful webhook: product deleted"); if (!wProductId) throw new NotFoundError("Missing webflow product ID"); await s.Commerce.Apparel.Syncs.Queue.enqueue({ type: "apparel_sync_delete", source: "printful", payload: { wProductId } }); break; } case Printful.Webhook.Event.PackageShipped: { const wOrderId = payload.data.order.external_id; const shipment = payload.data.shipment; log.info({ wOrderId, carrier: shipment.carrier, tracking: shipment.tracking_number }, "printful webhook: package shipped"); await s.Commerce.Apparel.Orders.Queue.enqueue({ type: "apparel_order_fulfill", source: "printful", payload: { wOrderId, shipment } }); break; } default: log.warn({ type: (payload as any).type }, "printful webhook: unhandled event type"); } }) .post("/webhooks/webflow", async ({ request, body }) => { if (!s.Commerce.Webflow.Util.verifyWebflowSignature(request, body)) throw status(400, "Invalid signature"); const payload = body as Webflow.Webhook.EventPayload; switch (payload.triggerType) { case Webflow.Webhook.Event.OrderCreated: { const wOrder = payload.payload; log.info({ orderId: wOrder.orderId }, "webflow webhook: order created"); await s.Commerce.Apparel.Orders.Queue.enqueue({ type: "apparel_order_create", source: "webflow", payload: { wOrder } }); break; } default: log.warn({ triggerType: (payload as any).triggerType }, "webflow webhook: unhandled event type"); } }); app.listen(3000, async () => { if (cluster.worker?.id === 1) { log.info({ port: 3000 }, "server started") // MANAGE QUEUES for (const queue of queues) { (async () => { while (true) { try { // clean, if ready if ((Date.now() - queue.lastCleanDate.getTime()) >= EventsService.CONCURRENCY_TIMEOUT_MS) await queue.clean().catch((err) => log.error({ name: queue.group, err })); // drain await queue.drain(); } catch (err) { log.error({ name: queue.group, err }, "error occurred during queue management"); } await sleep(EVENT_QUEUE_MANAGE_DELAY_MS); } })(); } } }); export type API = typeof app