feat: dynamic manifest in Cloudflare Worker with cookie-based org identification
Adds parseOrgIdFromCookie and buildManifest exports to the worker, intercepting /manifest.webmanifest to serve org-specific PWA icon URLs based on the x-org-id cookie. Removes legacy @ts-nocheck and biome-ignore-file comments. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
538d6e39e6
commit
595b330741
2 changed files with 112 additions and 7 deletions
37
apps/main/worker/index.test.ts
Normal file
37
apps/main/worker/index.test.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { buildManifest, parseOrgIdFromCookie } from "./index";
|
||||
|
||||
describe("parseOrgIdFromCookie", () => {
|
||||
it("returns null when no cookie header", () => {
|
||||
expect(parseOrgIdFromCookie(null)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null when x-org-id cookie is missing", () => {
|
||||
expect(parseOrgIdFromCookie("other=value; foo=bar")).toBeNull();
|
||||
});
|
||||
|
||||
it("parses x-org-id from cookie string", () => {
|
||||
expect(parseOrgIdFromCookie("x-org-id=42; other=value")).toBe(42);
|
||||
});
|
||||
|
||||
it("returns null for non-numeric x-org-id", () => {
|
||||
expect(parseOrgIdFromCookie("x-org-id=abc")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildManifest", () => {
|
||||
it("returns default icons when orgId is null", () => {
|
||||
const manifest = buildManifest(null);
|
||||
expect(manifest.name).toBe("XTablo");
|
||||
expect(manifest.icons[0].src).toBe("/pwa-icons/pwa-192x192.png");
|
||||
});
|
||||
|
||||
it("returns org-specific icon URLs when orgId is provided", () => {
|
||||
const manifest = buildManifest(42);
|
||||
expect(manifest.name).toBe("XTablo");
|
||||
expect(manifest.icons[0].src).toBe("/api/v1/public/org-icons/42/icon-192.png");
|
||||
expect(manifest.icons[1].src).toBe("/api/v1/public/org-icons/42/icon-512.png");
|
||||
expect(manifest.icons[2].src).toBe("/api/v1/public/org-icons/42/icon-512-maskable.png");
|
||||
expect(manifest.icons[2].purpose).toBe("maskable");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,16 +1,84 @@
|
|||
// @ts-nocheck
|
||||
// biome-ignore-file: Worker entry point with dynamic imports
|
||||
/* eslint-disable */
|
||||
export function parseOrgIdFromCookie(cookieHeader: string | null): number | null {
|
||||
if (!cookieHeader) return null;
|
||||
const match = cookieHeader.match(/(?:^|;\s*)x-org-id=(\d+)/);
|
||||
if (!match) return null;
|
||||
const id = Number.parseInt(match[1], 10);
|
||||
return Number.isNaN(id) ? null : id;
|
||||
}
|
||||
|
||||
interface ManifestIcon {
|
||||
src: string;
|
||||
sizes: string;
|
||||
type: string;
|
||||
purpose?: string;
|
||||
}
|
||||
|
||||
interface WebAppManifest {
|
||||
name: string;
|
||||
short_name: string;
|
||||
description: string;
|
||||
start_url: string;
|
||||
display: string;
|
||||
orientation: string;
|
||||
theme_color: string;
|
||||
background_color: string;
|
||||
icons: ManifestIcon[];
|
||||
}
|
||||
|
||||
export function buildManifest(orgId: number | null): WebAppManifest {
|
||||
const base = {
|
||||
name: "XTablo",
|
||||
short_name: "XTablo",
|
||||
description: "Collaborative project management for construction teams",
|
||||
start_url: "/",
|
||||
display: "standalone",
|
||||
orientation: "any",
|
||||
theme_color: "#1e1b2e",
|
||||
background_color: "#1e1b2e",
|
||||
};
|
||||
|
||||
if (orgId !== null) {
|
||||
return {
|
||||
...base,
|
||||
icons: [
|
||||
{ src: `/api/v1/public/org-icons/${orgId}/icon-192.png`, sizes: "192x192", type: "image/png" },
|
||||
{ src: `/api/v1/public/org-icons/${orgId}/icon-512.png`, sizes: "512x512", type: "image/png" },
|
||||
{ src: `/api/v1/public/org-icons/${orgId}/icon-512-maskable.png`, sizes: "512x512", type: "image/png", purpose: "maskable" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...base,
|
||||
icons: [
|
||||
{ src: "/pwa-icons/pwa-192x192.png", sizes: "192x192", type: "image/png" },
|
||||
{ src: "/pwa-icons/pwa-512x512.png", sizes: "512x512", type: "image/png" },
|
||||
{ src: "/pwa-icons/pwa-512x512-maskable.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
fetch(request) {
|
||||
fetch(request: Request) {
|
||||
const url = new URL(request.url);
|
||||
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
return Response.json({
|
||||
name: "Cloudflare",
|
||||
if (url.pathname === "/manifest.webmanifest") {
|
||||
const cookieHeader = request.headers.get("cookie");
|
||||
const orgId = parseOrgIdFromCookie(cookieHeader);
|
||||
const manifest = buildManifest(orgId);
|
||||
|
||||
return new Response(JSON.stringify(manifest), {
|
||||
headers: {
|
||||
"Content-Type": "application/manifest+json",
|
||||
"Cache-Control": "no-cache",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/api/")) {
|
||||
return Response.json({ name: "Cloudflare" });
|
||||
}
|
||||
|
||||
return new Response(null, { status: 404 });
|
||||
},
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue