refactor: update API routing to use /proxy prefix for same-origin requests
This commit is contained in:
@@ -16,10 +16,11 @@ export const getRequestBaseUrl = () => {
|
|||||||
return process.env.PRIVATE_SERVER_URL || env.PUBLIC_SERVER_URL;
|
return process.env.PRIVATE_SERVER_URL || env.PUBLIC_SERVER_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browser: same-origin via the Cloudflare Worker /api proxy. Keeps auth
|
// Browser: same-origin via the Cloudflare Worker `/proxy` prefix. Keeps
|
||||||
// cookies host-only on whichever domain the user is currently visiting
|
// auth cookies host-only on whichever domain the user is currently
|
||||||
// (app.classroomio.com, <org>.classroomio.school, or a BYOD domain).
|
// visiting (app.classroomio.com, <org>.classroomio.school, or BYOD).
|
||||||
return `${window.location.origin}/api`;
|
// The Worker strips `/proxy` and forwards the remainder to the API.
|
||||||
|
return `${window.location.origin}/proxy`;
|
||||||
};
|
};
|
||||||
|
|
||||||
function mergeAbortSignals(...signals: Array<AbortSignal | null | undefined>) {
|
function mergeAbortSignals(...signals: Array<AbortSignal | null | undefined>) {
|
||||||
|
|||||||
@@ -2,16 +2,25 @@ import { createAuthClient } from 'better-auth/svelte';
|
|||||||
import { env } from '$env/dynamic/public';
|
import { env } from '$env/dynamic/public';
|
||||||
import { ssoClient } from '@better-auth/sso/client';
|
import { ssoClient } from '@better-auth/sso/client';
|
||||||
|
|
||||||
// Browser: same-origin `/api` via the Cloudflare Worker, so auth cookies
|
// Browser: same-origin via the Cloudflare Worker, so auth cookies stay
|
||||||
// stay host-only on whichever domain the user is currently on
|
// host-only on whichever domain the user is on (app.classroomio.com,
|
||||||
// (app.classroomio.com, <org>.classroomio.school, or a BYOD domain).
|
// <org>.classroomio.school, or a BYOD domain). The URL is
|
||||||
// SSR: `authClient`'s methods aren't called server-side — the dashboard uses
|
// `<origin>/proxy/api/auth`:
|
||||||
// `authServerClient` for that — but Better Auth's constructor still validates
|
// - `/proxy` is the Worker's same-origin escape hatch — it strips
|
||||||
// the baseURL with `new URL(...)`, so we hand it the public API URL as a
|
// that prefix before forwarding to the upstream API.
|
||||||
// placeholder. PRIVATE_SERVER_URL is deliberately not used here: it's the
|
// - `/api/auth` is where Better Auth is mounted on the Hono API.
|
||||||
// internal Render hostname, never reachable from a browser.
|
// Better Auth's client treats any baseURL with a path as the auth root
|
||||||
|
// verbatim (it won't auto-append `/api/auth`), so we encode the full
|
||||||
|
// downstream path here.
|
||||||
|
//
|
||||||
|
// SSR: `authClient` methods are never invoked server-side (the dashboard
|
||||||
|
// uses `authServerClient` for that), but Better Auth's constructor still
|
||||||
|
// validates baseURL with `new URL(...)`, so we hand it the public API
|
||||||
|
// URL as an absolute-URL placeholder.
|
||||||
const baseURL =
|
const baseURL =
|
||||||
typeof window !== 'undefined' ? `${window.location.origin}/api` : env.PUBLIC_SERVER_URL || 'http://localhost:3002';
|
typeof window !== 'undefined'
|
||||||
|
? `${window.location.origin}/proxy/api/auth`
|
||||||
|
: env.PUBLIC_SERVER_URL || 'http://localhost:3002';
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL,
|
baseURL,
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ The Worker forwards each request to one of two Render services based on path:
|
|||||||
|
|
||||||
| Path | Upstream |
|
| Path | Upstream |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/api/*` | API service (`/api` prefix stripped) |
|
| `/proxy/*` | API service (`/proxy` prefix stripped, rest forwarded as-is) |
|
||||||
| anything else | Dashboard service |
|
| anything else | Dashboard service |
|
||||||
|
|
||||||
|
The `/proxy` prefix is the same-origin escape hatch the dashboard's
|
||||||
|
browser code uses so its auth cookies stay host-only while still reaching
|
||||||
|
the API. Example: a browser request to
|
||||||
|
`https://app.classroomio.com/proxy/api/auth/sign-in/email` becomes
|
||||||
|
`/api/auth/sign-in/email` on the API service (where Better Auth lives).
|
||||||
|
|
||||||
It preserves the original host as `X-Forwarded-Host` so:
|
It preserves the original host as `X-Forwarded-Host` so:
|
||||||
|
|
||||||
- The dashboard's SvelteKit adapter-node reads it via `HOST_HEADER=x-forwarded-host` and `event.url.host` becomes the tenant/admin/BYOD host.
|
- The dashboard's SvelteKit adapter-node reads it via `HOST_HEADER=x-forwarded-host` and `event.url.host` becomes the tenant/admin/BYOD host.
|
||||||
|
|||||||
@@ -8,13 +8,19 @@
|
|||||||
* - <BYOD>/* — customer-owned domains via Cloudflare for SaaS
|
* - <BYOD>/* — customer-owned domains via Cloudflare for SaaS
|
||||||
*
|
*
|
||||||
* Per request, the Worker decides upstream by path:
|
* Per request, the Worker decides upstream by path:
|
||||||
* - /api/* → API service (strips the /api prefix)
|
* - /proxy/* → API service (strips the /proxy prefix, forwards the rest)
|
||||||
* - else → Dashboard service
|
* - else → Dashboard service
|
||||||
*
|
*
|
||||||
* It preserves the original Host as X-Forwarded-Host so the dashboard
|
* The `/proxy` prefix is the same-origin escape hatch used by the
|
||||||
* (SvelteKit adapter-node `HOST_HEADER`) and Better Auth see the host the
|
* dashboard's browser code so its auth cookies stay host-only while still
|
||||||
* browser actually called. Auth cookies are emitted without a Domain
|
* reaching the API. Any path after `/proxy` is the real API path on the
|
||||||
* attribute upstream, so they scope host-only on every domain naturally.
|
* Hono server — e.g. `/proxy/api/auth/sign-in/email` strips to
|
||||||
|
* `/api/auth/sign-in/email`, which is where Better Auth is mounted.
|
||||||
|
*
|
||||||
|
* The Worker preserves the original Host as X-Forwarded-Host so the
|
||||||
|
* dashboard (SvelteKit adapter-node `HOST_HEADER`) and Better Auth see the
|
||||||
|
* host the browser actually called. Auth cookies are emitted without a
|
||||||
|
* Domain attribute upstream, so they scope host-only on every domain.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface Env {
|
interface Env {
|
||||||
@@ -23,7 +29,7 @@ interface Env {
|
|||||||
APEX_REDIRECT_TARGET: string;
|
APEX_REDIRECT_TARGET: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_PREFIX = '/api';
|
const PROXY_PREFIX = '/proxy';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async fetch(request: Request, env: Env): Promise<Response> {
|
async fetch(request: Request, env: Env): Promise<Response> {
|
||||||
@@ -38,9 +44,9 @@ export default {
|
|||||||
return Response.redirect(target.toString(), 301);
|
return Response.redirect(target.toString(), 301);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isApiCall = url.pathname === API_PREFIX || url.pathname.startsWith(`${API_PREFIX}/`);
|
const isApiCall = url.pathname === PROXY_PREFIX || url.pathname.startsWith(`${PROXY_PREFIX}/`);
|
||||||
const upstreamHost = isApiCall ? env.API_UPSTREAM_HOST : env.DASHBOARD_UPSTREAM_HOST;
|
const upstreamHost = isApiCall ? env.API_UPSTREAM_HOST : env.DASHBOARD_UPSTREAM_HOST;
|
||||||
const upstreamPath = isApiCall ? url.pathname.slice(API_PREFIX.length) || '/' : url.pathname;
|
const upstreamPath = isApiCall ? url.pathname.slice(PROXY_PREFIX.length) || '/' : url.pathname;
|
||||||
|
|
||||||
const upstreamUrl = new URL(request.url);
|
const upstreamUrl = new URL(request.url);
|
||||||
upstreamUrl.protocol = 'https:';
|
upstreamUrl.protocol = 'https:';
|
||||||
|
|||||||
Reference in New Issue
Block a user