import { writeFileSync, mkdirSync } from "fs"; mkdirSync("lib", { recursive: true }); // ─── lib/api.ts ───────────────────────────────────────────────────────────── writeFileSync("lib/api.ts", `// Client-side fetch helper with automatic Bearer token injection and 401 auto-refresh. export interface ApiResponse { data: T; meta: { timestamp: string; version: "v1" }; error: null | { message: string; code?: string }; } const TOKEN_KEY = "ledgerone_token"; const REFRESH_KEY = "ledgerone_refresh_token"; const USER_KEY = "ledgerone_user"; export function getStoredToken(): string { if (typeof window === "undefined") return ""; return localStorage.getItem(TOKEN_KEY) ?? ""; } export function getStoredUser(): T | null { if (typeof window === "undefined") return null; try { const raw = localStorage.getItem(USER_KEY); return raw ? (JSON.parse(raw) as T) : null; } catch { return null; } } export function storeAuthTokens(data: { accessToken: string; refreshToken: string; user: unknown; }): void { localStorage.setItem(TOKEN_KEY, data.accessToken); localStorage.setItem(REFRESH_KEY, data.refreshToken); localStorage.setItem(USER_KEY, JSON.stringify(data.user)); // Set a non-HttpOnly cookie so Next.js middleware can detect auth state document.cookie = "ledgerone_auth=1; path=/; max-age=2592000; SameSite=Lax"; } export function clearAuth(): void { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REFRESH_KEY); localStorage.removeItem(USER_KEY); document.cookie = "ledgerone_auth=; path=/; max-age=0"; } async function tryRefresh(): Promise { const refreshToken = localStorage.getItem(REFRESH_KEY); if (!refreshToken) return null; try { const res = await fetch("/api/auth/refresh", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }), }); if (!res.ok) { clearAuth(); return null; } const payload = (await res.json()) as ApiResponse<{ accessToken: string; refreshToken: string; }>; if (payload.error || !payload.data?.accessToken) { clearAuth(); return null; } localStorage.setItem(TOKEN_KEY, payload.data.accessToken); localStorage.setItem(REFRESH_KEY, payload.data.refreshToken); return payload.data.accessToken; } catch { clearAuth(); return null; } } export async function apiFetch( path: string, options: RequestInit = {} ): Promise> { const token = getStoredToken(); const headers: Record = { ...(options.headers as Record), }; if (token) headers["Authorization"] = \`Bearer \${token}\`; if ( options.body && typeof options.body === "string" && !headers["Content-Type"] ) { headers["Content-Type"] = "application/json"; } let res = await fetch(path, { ...options, headers }); // Auto-refresh on 401 if (res.status === 401) { const newToken = await tryRefresh(); if (newToken) { headers["Authorization"] = \`Bearer \${newToken}\`; res = await fetch(path, { ...options, headers }); } else { if (typeof window !== "undefined") { window.location.href = "/login"; } return { data: null as T, meta: { timestamp: new Date().toISOString(), version: "v1" }, error: { message: "Session expired. Please sign in again." }, }; } } return res.json() as Promise>; } `); // ─── lib/backend.ts ────────────────────────────────────────────────────────── writeFileSync("lib/backend.ts", `// Server-side proxy helper for Next.js API routes. // Forwards requests to the NestJS backend, including the Bearer token. import { NextRequest, NextResponse } from "next/server"; const BASE_URL = process.env.LEDGERONE_API_URL ?? "http://localhost:3051"; export function getBackendUrl(path: string): string { return \`\${BASE_URL}/api/\${path.replace(/^\\//, "")}\`; } interface ProxyOptions { method?: string; body?: BodyInit | null; extraHeaders?: Record; search?: string; } export async function proxyRequest( req: NextRequest, backendPath: string, options: ProxyOptions = {} ): Promise { const url = new URL(req.url); const search = options.search !== undefined ? options.search : url.search; const targetUrl = \`\${getBackendUrl(backendPath)}\${search}\`; const method = options.method ?? req.method; const auth = req.headers.get("authorization") ?? ""; const contentType = req.headers.get("content-type") ?? ""; const headers: Record = { ...options.extraHeaders }; if (auth) headers["Authorization"] = auth; let body: BodyInit | null | undefined = undefined; if (method !== "GET" && method !== "HEAD") { if (options.body !== undefined) { body = options.body; if (contentType) headers["Content-Type"] = contentType; } else if (contentType.includes("multipart/form-data")) { body = await req.formData(); // Do not set Content-Type — fetch sets it with boundary automatically } else { body = await req.text(); if (contentType) headers["Content-Type"] = contentType; } } try { const res = await fetch(targetUrl, { method, headers, body: body ?? undefined, }); const payload = await res.text(); return new NextResponse(payload, { status: res.status, headers: { "Content-Type": res.headers.get("content-type") ?? "application/json", }, }); } catch { return NextResponse.json( { data: null, meta: { timestamp: new Date().toISOString(), version: "v1" }, error: { message: "Backend unavailable." }, }, { status: 503 } ); } } `); // ─── middleware.ts ─────────────────────────────────────────────────────────── writeFileSync("middleware.ts", `import { NextRequest, NextResponse } from "next/server"; const PROTECTED_PREFIXES = [ "/app", "/transactions", "/rules", "/exports", "/tax", "/settings", "/profile", ]; export function middleware(req: NextRequest) { const { pathname } = req.nextUrl; const isProtected = PROTECTED_PREFIXES.some( (prefix) => pathname === prefix || pathname.startsWith(\`\${prefix}/\`) ); if (!isProtected) return NextResponse.next(); // ledgerone_auth cookie is set by storeAuthTokens() in lib/api.ts const authCookie = req.cookies.get("ledgerone_auth"); if (!authCookie?.value) { const loginUrl = new URL("/login", req.url); loginUrl.searchParams.set("next", pathname); return NextResponse.redirect(loginUrl); } return NextResponse.next(); } export const config = { matcher: [ // Run on all routes except Next.js internals and static files "/((?!_next/static|_next/image|favicon\\.ico|api/).*)", ], }; `); console.log("✅ lib/api.ts, lib/backend.ts, middleware.ts written");