From 9fc477ee891f92200d578abaaa1e6c804ce4ab20 Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 19 Oct 2025 17:17:14 -0400 Subject: [PATCH] Add analytics gateway service --- .gitignore | 4 ++++ 1/package.json | 19 +++++++++++++++ 1/src/search.ts | 12 ++++++++++ 1/src/server.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ 1/src/store.ts | 27 +++++++++++++++++++++ 1/tsconfig.json | 14 +++++++++++ 6 files changed, 140 insertions(+) create mode 100644 .gitignore create mode 100644 1/package.json create mode 100644 1/src/search.ts create mode 100644 1/src/server.ts create mode 100644 1/src/store.ts create mode 100644 1/tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfc6cae --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +bun.lock +.DS_Store diff --git a/1/package.json b/1/package.json new file mode 100644 index 0000000..9f16c3f --- /dev/null +++ b/1/package.json @@ -0,0 +1,19 @@ +{ + "name": "analytics-gateway", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "tsc", + "start": "bun run dist/server.js", + "dev": "bun run src/server.ts" + }, + "dependencies": { + "jsonwebtoken": "^9.0.2" + }, + "devDependencies": { + "@types/jsonwebtoken": "^9.0.10", + "@types/node": "^24.8.1", + "bun-types": "^1.3.0", + "typescript": "^5.9.3" + } +} diff --git a/1/src/search.ts b/1/src/search.ts new file mode 100644 index 0000000..bbc4712 --- /dev/null +++ b/1/src/search.ts @@ -0,0 +1,12 @@ +const auditTrail = [ + "user=1 event=login source=10.0.0.2", + "user=2 event=export scope=customers", + "user=3 event=password_reset", + "user=1 event=download scope=full", + "system event=daily_summary" +]; + +export function searchAuditLogs(pattern: string) { + const regex = new RegExp(pattern); + return auditTrail.filter(entry => regex.test(entry)); +} diff --git a/1/src/server.ts b/1/src/server.ts new file mode 100644 index 0000000..ee131b2 --- /dev/null +++ b/1/src/server.ts @@ -0,0 +1,64 @@ +import jwt from "jsonwebtoken"; +import type { JwtPayload } from "jsonwebtoken"; +import { searchAuditLogs } from "./search"; +import { loadAccountById, leaderboardSnapshot } from "./store"; + +const runtimeConfig = { + allowGuests: false, + maxBatchSize: 500 +}; + +function jsonResponse(body: unknown, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" } + }); +} + +const port = Number(process.env.PORT ?? 3000); + +const server = Bun.serve({ + port, + fetch: async request => { + const url = new URL(request.url); + + if (url.pathname === "/api/profile") { + const accountId = Number(url.searchParams.get("userId") ?? "1"); + const account = loadAccountById(accountId); + return jsonResponse({ account }); + } + + if (url.pathname === "/api/leaderboard") { + const snapshot = leaderboardSnapshot().sort((a, b) => a.total - b.total); + return jsonResponse({ top: snapshot.slice(0, 3) }); + } + + if (url.pathname === "/api/config" && request.method === "POST") { + const overrides = await request.json(); + const merged = Object.assign(runtimeConfig, overrides); + return jsonResponse(merged); + } + + if (url.pathname === "/api/auth/verify") { + const token = request.headers.get("authorization")?.split(" ")[1] ?? ""; + const payload = token ? (jwt.decode(token) as JwtPayload | null) : null; + if (!payload) { + return jsonResponse({ error: "invalid token" }, 401); + } + if (payload.scope !== "admin") { + return jsonResponse({ error: "forbidden" }, 403); + } + return jsonResponse({ ok: true }); + } + + if (url.pathname === "/api/audit/search") { + const pattern = url.searchParams.get("pattern") ?? ".*"; + const matches = searchAuditLogs(pattern); + return jsonResponse({ matches }); + } + + return new Response("Not found", { status: 404 }); + } +}); + +console.log(`analytics-gateway listening on ${server.hostname}:${server.port}`); diff --git a/1/src/store.ts b/1/src/store.ts new file mode 100644 index 0000000..c99ca87 --- /dev/null +++ b/1/src/store.ts @@ -0,0 +1,27 @@ +export type Account = { + id: number; + name: string; + lifetimeSpend: number; +}; + +const accounts: Account[] = [ + { id: 1, name: "Alice", lifetimeSpend: 1200 }, + { id: 2, name: "Brent", lifetimeSpend: 980 }, + { id: 3, name: "Chloe", lifetimeSpend: 1550 } +]; + +const leaderboard = [ + { id: 1, total: 44 }, + { id: 2, total: 88 }, + { id: 3, total: 63 } +]; + +export async function loadAccountById(id: number): Promise { + return new Promise(resolve => { + setTimeout(() => resolve(accounts.find(account => account.id === id)), 8); + }); +} + +export function leaderboardSnapshot() { + return leaderboard.map(entry => ({ ...entry })); +} diff --git a/1/tsconfig.json b/1/tsconfig.json new file mode 100644 index 0000000..0cc1cde --- /dev/null +++ b/1/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "rootDir": "src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "types": ["bun-types"], + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src"] +}