From d6b845e6d2e41777813923e05cffbdd1cc924ea2 Mon Sep 17 00:00:00 2001 From: Corey Johnson Date: Thu, 10 Jul 2025 16:14:18 -0700 Subject: [PATCH] Auth --- packages/http/src/server.tsx | 146 ++++++++++++++++++++++++++--------- 1 file changed, 111 insertions(+), 35 deletions(-) diff --git a/packages/http/src/server.tsx b/packages/http/src/server.tsx index cdcea4b..622ad43 100644 --- a/packages/http/src/server.tsx +++ b/packages/http/src/server.tsx @@ -1,47 +1,123 @@ import { startSubdomainServers } from "@/orchestrator" import { nanoRemix } from "@workshop/nano-remix" -import { serve } from "bun" import { join } from "node:path" -const portMap = await startSubdomainServers() +/** + * Password-protected subdomain proxy server + * + * How it works: + * 1. Starts multiple subdomain servers on different ports (3001+) + * 2. Main server (port 3000) acts as a proxy that routes requests based on subdomain + * 3. All requests are protected with Basic HTTP auth + persistent cookies + * 4. Once authenticated, cookies work across all subdomains (*.yourdomain.com) + * + * This server is designed to be run in production with NODE_ENV=production. On development, + * it allows unauthenticated access because YOLO. + */ -const server = serve({ - port: 3000, - fetch: async (req) => { - const url = new URL(req.url) - const hostname = url.hostname +const serve = async () => { + const portMap = await startSubdomainServers() + const server = startServer(portMap) + logServerInfo(server, portMap) +} - const subdomain = hostname.split(".")[0] +serve() - const targetPort = subdomain && portMap[subdomain] - if (!targetPort) { - const routePath = join(import.meta.dir, "routes") - return nanoRemix(req, { routePath }) +const startServer = (portMap: Record) => { + const server = Bun.serve({ + port: 3000, + routes: { + // Handle main site routes through nano-remix + "/*": requireAuth(async (req) => { + const url = new URL(req.url) + const hostname = url.hostname + const subdomain = hostname.split(".")[0] + + const targetPort = subdomain && portMap[subdomain] + if (!targetPort) { + const routePath = join(import.meta.dir, "routes") + return nanoRemix(req, { routePath }) + } + + try { + const target = `http://127.0.0.1:${targetPort}${url.pathname}${url.search}` + + // Forward the request with auth headers preserved + const forwardedReq = new Request(target, { + method: req.method, + headers: req.headers, + body: req.body, + }) + + const upstream = await fetch(forwardedReq) + + return new Response(upstream.body, { + status: upstream.status, + headers: new Headers(upstream.headers), + }) + } catch (error) { + console.error(`Error forwarding request to subdomain "${subdomain}":`, error) + return new Response(`Failed to forward request to "${subdomain}"`, { status: 500 }) + } + }), + }, + }) + + return server +} + +const logServerInfo = (server: Bun.Server, portMap: Record) => { + console.log(`${server.url}`) + const subdomainEntries = Object.entries(portMap) + subdomainEntries.forEach(([subdomain, port], index) => { + const subdomainUrl = new URL(server.url) + subdomainUrl.hostname = `${subdomain}.${subdomainUrl.hostname}` + subdomainUrl.port = port.toString() + const isLast = index === subdomainEntries.length - 1 + const prefix = isLast ? "└─" : "├─" + console.log(`${prefix} ${subdomainUrl}`) + }) + console.log("") +} + +const requireAuth = (handler: (req: Request) => Response | Promise) => { + return async (req: Request) => { + if (process.env.NODE_ENV !== "production") { + return handler(req) } - try { - const target = `http://127.0.0.1:${targetPort}${url.pathname}${url.search}` - const upstream = await fetch(target, req) + const cookies = new Bun.CookieMap(req.headers.get("cookie") ?? "") + if (cookies.get("auth") === "authenticated") { + return handler(req) + } - return new Response(upstream.body, { - status: upstream.status, - headers: new Headers(upstream.headers), + const auth = req.headers.get("authorization") + const correctAuth = `Basic ${btoa("spike:888")}` + + if (auth === correctAuth) { + const response = await handler(req) + const newHeaders = new Headers(response.headers) + + const url = new URL(req.url) + const cookieDomain = `.${url.hostname.split(".").slice(-2).join(".")}` + + const authCookie = new Bun.Cookie({ + name: "auth", + value: "authenticated", + domain: cookieDomain, + path: "/", + maxAge: 86400, + httpOnly: true, }) - } catch (error) { - console.error(`Error forwarding request to subdomain "${subdomain}":`, error) - return new Response(`Failed to forward request to "${subdomain}"`, { status: 500 }) - } - }, -}) -console.log(`${server.url}`) -const subdomainEntries = Object.entries(portMap) -subdomainEntries.forEach(([subdomain, port], index) => { - const subdomainUrl = new URL(server.url) - subdomainUrl.hostname = `${subdomain}.${subdomainUrl.hostname}` - subdomainUrl.port = port.toString() - const isLast = index === subdomainEntries.length - 1 - const prefix = isLast ? "└─" : "├─" - console.log(`${prefix} ${subdomainUrl}`) -}) -console.log("") + newHeaders.set("Set-Cookie", authCookie.toString()) + + return new Response(response.body, { status: response.status, headers: newHeaders }) + } + + return new Response("Unauthorized", { + status: 401, + headers: { "WWW-Authenticate": 'Basic realm="Workshop Access"' }, + }) + } +}