fix: undo org search from tenant-router
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { getFirstOrg, getOrgBySiteName, getOrgsByCustomDomain } from '$features/org/api/org.server';
|
||||
|
||||
import { dev } from '$app/environment';
|
||||
import type { AccountOrg } from '$features/app/types';
|
||||
import type { Cookies } from '@sveltejs/kit';
|
||||
import { PUBLIC_IS_SELFHOSTED } from '$env/static/public';
|
||||
@@ -54,10 +53,15 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise<OrgSit
|
||||
// Custom domain
|
||||
if (isURLCustomDomain(url)) {
|
||||
console.log('it is custom domain');
|
||||
const org = await lookupOrgFromWorker(url.origin, { customDomain: url.host });
|
||||
const apiKeyHeaders = getApiKeyHeaders();
|
||||
const orgs = await getOrgsByCustomDomain(url.host, true, apiKeyHeaders);
|
||||
console.log('orgs', orgs);
|
||||
|
||||
if (!org) return response;
|
||||
if (!orgs || orgs.length === 0) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const org = orgs[0];
|
||||
response.org = org as AccountOrg;
|
||||
response.isOrgSite = true;
|
||||
response.orgSiteName = response.org?.siteName || '';
|
||||
@@ -78,7 +82,9 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise<OrgSit
|
||||
response.orgSiteName = debugMode ? _orgSiteName : subdomain;
|
||||
|
||||
if (response.orgSiteName) {
|
||||
response.org = await lookupOrgFromWorker(url.origin, { siteName: response.orgSiteName });
|
||||
const apiKeyHeaders = getApiKeyHeaders();
|
||||
const org = await getOrgBySiteName(response.orgSiteName, apiKeyHeaders);
|
||||
response.org = org ?? null;
|
||||
}
|
||||
|
||||
const shouldDeleteCookie = !response.org && _orgSiteName;
|
||||
@@ -90,47 +96,6 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise<OrgSit
|
||||
return response;
|
||||
}
|
||||
|
||||
// Resolve the org via the tenant-router Worker. We call the *same origin* the
|
||||
// browser hit (e.g. https://<tenant>.myclassroomio.com) because that host is
|
||||
// fronted by the Worker, which serves /internal/org-lookup from KV (≈5ms) or
|
||||
// Hyperdrive→Postgres on a miss (≈50ms). No API/Render hop, no extra env var.
|
||||
//
|
||||
// In dev / self-hosted there is no Worker in front, so fall back to the
|
||||
// direct API call (same path the code used before the Worker cache existed).
|
||||
async function lookupOrgFromWorker(
|
||||
origin: string,
|
||||
params: { siteName?: string; customDomain?: string }
|
||||
): Promise<AccountOrg | null> {
|
||||
if (dev || PUBLIC_IS_SELFHOSTED === 'true' || origin.includes('localhost')) {
|
||||
const apiKeyHeaders = getApiKeyHeaders();
|
||||
if (params.siteName) {
|
||||
return (await getOrgBySiteName(params.siteName, apiKeyHeaders)) ?? null;
|
||||
}
|
||||
if (params.customDomain) {
|
||||
const orgs = await getOrgsByCustomDomain(params.customDomain, true, apiKeyHeaders);
|
||||
return (orgs?.[0] as AccountOrg) ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams();
|
||||
if (params.siteName) qs.set('siteName', params.siteName);
|
||||
if (params.customDomain) qs.set('customDomain', params.customDomain);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${origin}/internal/org-lookup?${qs}`, {
|
||||
headers: getApiKeyHeaders().headers
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
|
||||
const json = (await res.json()) as { success: boolean; data: AccountOrg | null };
|
||||
return json.success ? json.data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isURLCustomDomain(url: URL) {
|
||||
if (url.host.includes('localhost')) {
|
||||
return false;
|
||||
|
||||
@@ -12,11 +12,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250109.0",
|
||||
"@types/pg": "^8.16.0",
|
||||
"typescript": "^5.5.2",
|
||||
"wrangler": "4.21.x"
|
||||
},
|
||||
"dependencies": {
|
||||
"pg": "^8.18.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Client } from 'pg';
|
||||
|
||||
/**
|
||||
* @cio/tenant-router — Cloudflare Worker that fronts every browser-facing host.
|
||||
*
|
||||
@@ -45,21 +43,8 @@ interface Env {
|
||||
HLS_SIGNING_SECRET: string;
|
||||
/** R2 bucket binding for the videos bucket (HLS manifests + segments + audio). */
|
||||
VIDEOS_BUCKET: R2Bucket;
|
||||
/** KV namespace for caching org lookups (60 s TTL). */
|
||||
ORG_CACHE: KVNamespace;
|
||||
/** Hyperdrive binding for direct Postgres access. */
|
||||
HYPERDRIVE: Hyperdrive;
|
||||
/**
|
||||
* Shared secret the Dashboard SSR server sends as `Authorization: Bearer …`
|
||||
* to call /internal/org-lookup. Must equal the Dashboard's PRIVATE_SERVER_KEY.
|
||||
* Set via `wrangler secret put PRIVATE_SERVER_KEY`.
|
||||
*/
|
||||
PRIVATE_SERVER_KEY: string;
|
||||
}
|
||||
|
||||
const ORG_LOOKUP_PATH = '/internal/org-lookup';
|
||||
const ORG_CACHE_TTL = 60;
|
||||
|
||||
// PostHog first-party proxy: requests to /ingest/* are forwarded to PostHog EU.
|
||||
// This makes PostHog cookies first-party (set on the org's domain, not eu.i.posthog.com),
|
||||
// eliminating the "third-party cookie" Lighthouse Best Practices deduction.
|
||||
@@ -218,38 +203,8 @@ async function handleHlsRequest(request: Request, env: Env): Promise<Response> {
|
||||
return new Response((object as R2ObjectBody).body, { status: 200, headers });
|
||||
}
|
||||
|
||||
// Drizzle stores these columns in camelCase with no snake_case conversion, so
|
||||
// the identifiers are case-sensitive in Postgres and must be double-quoted.
|
||||
async function lookupOrgFromDb(
|
||||
hyperdrive: Hyperdrive,
|
||||
ctx: ExecutionContext,
|
||||
params: { siteName: string | null; customDomain: string | null }
|
||||
): Promise<Record<string, unknown> | null> {
|
||||
const client = new Client({ connectionString: hyperdrive.connectionString });
|
||||
await client.connect();
|
||||
|
||||
try {
|
||||
if (params.siteName) {
|
||||
const result = await client.query('SELECT * FROM organization WHERE "siteName" = $1 LIMIT 1', [params.siteName]);
|
||||
return result.rows[0] ?? null;
|
||||
}
|
||||
if (params.customDomain) {
|
||||
const result = await client.query(
|
||||
'SELECT * FROM organization WHERE "customDomain" = $1 AND "isCustomDomainVerified" = true LIMIT 1',
|
||||
[params.customDomain.toLowerCase()]
|
||||
);
|
||||
return result.rows[0] ?? null;
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
// Close in the background so connection teardown never blocks the response.
|
||||
ctx.waitUntil(client.end());
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
const url = new URL(request.url);
|
||||
const originalHost = request.headers.get('host') ?? url.host;
|
||||
|
||||
@@ -286,49 +241,6 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
// Dedicated org lookup endpoint called directly by the Dashboard SSR server.
|
||||
// Dashboard → Worker (KV hit: ~5ms | KV miss + Hyperdrive: ~50ms).
|
||||
// The API server and Render are never in this path.
|
||||
if (request.method === 'GET' && url.pathname === ORG_LOOKUP_PATH) {
|
||||
// Server-to-server only: require the shared secret. This endpoint is
|
||||
// reachable from the public internet, so without this anyone could read
|
||||
// org config by site name.
|
||||
const auth = request.headers.get('authorization');
|
||||
const expected = `Bearer ${env.PRIVATE_SERVER_KEY}`;
|
||||
if (!env.PRIVATE_SERVER_KEY || !auth || !timingSafeEqual(auth, expected)) {
|
||||
return new Response(JSON.stringify({ success: false, error: 'Unauthorized' }), {
|
||||
status: 401,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const siteName = url.searchParams.get('siteName');
|
||||
const customDomain = url.searchParams.get('customDomain');
|
||||
const lookupKey = siteName ? `siteName:${siteName}` : customDomain ? `customDomain:${customDomain}` : null;
|
||||
|
||||
if (!lookupKey) {
|
||||
return new Response(JSON.stringify({ success: false, error: 'siteName or customDomain required' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
const cached = await env.ORG_CACHE.get(lookupKey);
|
||||
if (cached !== null) {
|
||||
return new Response(cached, {
|
||||
headers: { 'Content-Type': 'application/json', 'X-Cache': 'HIT' }
|
||||
});
|
||||
}
|
||||
|
||||
const org = await lookupOrgFromDb(env.HYPERDRIVE, ctx, { siteName, customDomain });
|
||||
const body = JSON.stringify({ success: true, data: org });
|
||||
await env.ORG_CACHE.put(lookupKey, body, { expirationTtl: ORG_CACHE_TTL });
|
||||
|
||||
return new Response(body, {
|
||||
headers: { 'Content-Type': 'application/json', 'X-Cache': 'MISS' }
|
||||
});
|
||||
}
|
||||
|
||||
const isProxiedApiCall = url.pathname === PROXY_PREFIX || url.pathname.startsWith(`${PROXY_PREFIX}/`);
|
||||
const isDirectAuthCall = url.pathname === AUTH_PREFIX || url.pathname.startsWith(`${AUTH_PREFIX}/`);
|
||||
const isApiCall = isProxiedApiCall || isDirectAuthCall;
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
name = "cio-tenant-router"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2026-05-01"
|
||||
# Required for the `pg` (node-postgres) driver used to query Hyperdrive.
|
||||
compatibility_flags = ["nodejs_compat"]
|
||||
|
||||
# Production routes — bind the Worker to every browser-facing host.
|
||||
# Tenant subdomains and the apex live on the `myclassroomio.com` zone.
|
||||
@@ -34,16 +32,5 @@ bucket_name = "videos"
|
||||
# never commit it. The same value must be set as HLS_SIGNING_SECRET on the
|
||||
# API (Render env) so the cookie minted by the API verifies on the Worker.
|
||||
|
||||
# Org lookup cache. Create with:
|
||||
# wrangler kv namespace create ORG_CACHE
|
||||
# then paste the returned id below.
|
||||
[[kv_namespaces]]
|
||||
binding = "ORG_CACHE"
|
||||
id = "d07dc73ade284d41af53b387623df29b"
|
||||
|
||||
[[hyperdrive]]
|
||||
binding = "HYPERDRIVE"
|
||||
id = "9ecbfa45699e452889e72acc64af455b"
|
||||
|
||||
[observability]
|
||||
enabled = true
|
||||
|
||||
Generated
-7
@@ -779,17 +779,10 @@ importers:
|
||||
version: 4.2.1
|
||||
|
||||
apps/tenant-router:
|
||||
dependencies:
|
||||
pg:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0
|
||||
devDependencies:
|
||||
'@cloudflare/workers-types':
|
||||
specifier: ^4.20250109.0
|
||||
version: 4.20260511.1
|
||||
'@types/pg':
|
||||
specifier: ^8.16.0
|
||||
version: 8.16.0
|
||||
typescript:
|
||||
specifier: ^5.5.2
|
||||
version: 5.9.3
|
||||
|
||||
Reference in New Issue
Block a user