chore: simplify workflow for self hosters.
This commit is contained in:
@@ -16,10 +16,17 @@ 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 `/proxy` prefix. Keeps
|
// Self-hosted: dashboard and API are on different subdomains of the
|
||||||
// auth cookies host-only on whichever domain the user is currently
|
// operator's apex. Browser calls go straight to PUBLIC_SERVER_URL;
|
||||||
// visiting (app.classroomio.com, <org>.classroomio.school, or BYOD).
|
// cookies cross subdomains via AUTH_COOKIE_DOMAIN. No Worker proxy.
|
||||||
// The Worker strips `/proxy` and forwards the remainder to the API.
|
if (env.PUBLIC_IS_SELFHOSTED === 'true') {
|
||||||
|
return env.PUBLIC_SERVER_URL ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cloud (multi-tenant): same-origin via the Cloudflare Worker `/proxy`
|
||||||
|
// prefix so auth cookies stay host-only on whichever tenant or BYOD
|
||||||
|
// domain the user is currently visiting. The Worker strips `/proxy`
|
||||||
|
// before forwarding to the API.
|
||||||
return `${window.location.origin}/proxy`;
|
return `${window.location.origin}/proxy`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,25 +2,33 @@ 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 via the Cloudflare Worker, so auth cookies stay
|
// Browser baseURL depends on deployment shape:
|
||||||
// host-only on whichever domain the user is on (app.classroomio.com,
|
//
|
||||||
// <org>.classroomio.school, or a BYOD domain). The URL is
|
// - Self-hosted: dashboard and API are on different subdomains under
|
||||||
// `<origin>/proxy/api/auth`:
|
// one apex. Browser calls go straight to PUBLIC_SERVER_URL; cookies
|
||||||
// - `/proxy` is the Worker's same-origin escape hatch — it strips
|
// cross subdomains via AUTH_COOKIE_DOMAIN. No Worker proxy.
|
||||||
// that prefix before forwarding to the upstream API.
|
//
|
||||||
// - `/api/auth` is where Better Auth is mounted on the Hono API.
|
// - Cloud (multi-tenant): same-origin via the Cloudflare Worker so
|
||||||
// Better Auth's client treats any baseURL with a path as the auth root
|
// auth cookies stay host-only on whichever tenant/BYOD domain the
|
||||||
// verbatim (it won't auto-append `/api/auth`), so we encode the full
|
// user is on. We encode `<origin>/proxy/api/auth` because Better
|
||||||
// downstream path here.
|
// Auth's client treats any baseURL with a path as the full auth
|
||||||
|
// root verbatim (won't auto-append `/api/auth`).
|
||||||
//
|
//
|
||||||
// SSR: `authClient` methods are never invoked server-side (the dashboard
|
// SSR: `authClient` methods are never invoked server-side (the dashboard
|
||||||
// uses `authServerClient` for that), but Better Auth's constructor still
|
// uses `authServerClient` for that), but Better Auth's constructor still
|
||||||
// validates baseURL with `new URL(...)`, so we hand it the public API
|
// validates baseURL with `new URL(...)`, so we hand it the public API
|
||||||
// URL as an absolute-URL placeholder.
|
// URL as an absolute-URL placeholder.
|
||||||
const baseURL =
|
function resolveBaseURL() {
|
||||||
typeof window !== 'undefined'
|
if (typeof window === 'undefined') {
|
||||||
? `${window.location.origin}/proxy/api/auth`
|
return env.PUBLIC_SERVER_URL || 'http://localhost:3002';
|
||||||
: env.PUBLIC_SERVER_URL || 'http://localhost:3002';
|
}
|
||||||
|
if (env.PUBLIC_IS_SELFHOSTED === 'true') {
|
||||||
|
return env.PUBLIC_SERVER_URL || `${window.location.origin}/api/auth`;
|
||||||
|
}
|
||||||
|
return `${window.location.origin}/proxy/api/auth`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseURL = resolveBaseURL();
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
baseURL,
|
baseURL,
|
||||||
|
|||||||
+23
-9
@@ -19,6 +19,22 @@ import { syncUserWithProfile } from './auth/hooks/sync-user';
|
|||||||
import { tokenExchange } from './auth/plugins/token-exchange';
|
import { tokenExchange } from './auth/plugins/token-exchange';
|
||||||
import { trackLoginHook } from './auth/hooks/track-login';
|
import { trackLoginHook } from './auth/hooks/track-login';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cloud (multi-tenant) only. Routes OAuth/SSO callbacks to the canonical
|
||||||
|
* production URL while completing the flow on whichever tenant host the
|
||||||
|
* user signed in from (<org>.classroomio.school or a BYOD domain).
|
||||||
|
*
|
||||||
|
* Self-hosted instances run with one apex (api.<domain> + app.<domain>)
|
||||||
|
* and use AUTH_COOKIE_DOMAIN for cross-subdomain cookies, so the proxy
|
||||||
|
* isn't needed there.
|
||||||
|
*/
|
||||||
|
function buildOAuthProxyPlugin() {
|
||||||
|
if (process.env.PUBLIC_IS_SELFHOSTED === 'true') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [oAuthProxy({ productionURL: CONSTANTS.BASE_URL })];
|
||||||
|
}
|
||||||
|
|
||||||
export const auth: ReturnType<typeof betterAuth> = betterAuth({
|
export const auth: ReturnType<typeof betterAuth> = betterAuth({
|
||||||
baseURL: CONSTANTS.BASE_URL,
|
baseURL: CONSTANTS.BASE_URL,
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
@@ -58,7 +74,12 @@ export const auth: ReturnType<typeof betterAuth> = betterAuth({
|
|||||||
},
|
},
|
||||||
advanced: {
|
advanced: {
|
||||||
cookiePrefix: 'classroomio',
|
cookiePrefix: 'classroomio',
|
||||||
crossSubDomainCookies: { enabled: false },
|
// Cloud (multi-tenant): host-only cookies on each tenant/BYOD domain.
|
||||||
|
// Self-hosted: cross-subdomain cookies under AUTH_COOKIE_DOMAIN so the
|
||||||
|
// session set on `api.<apex>` is also sent to `app.<apex>`.
|
||||||
|
crossSubDomainCookies: process.env.AUTH_COOKIE_DOMAIN?.trim()
|
||||||
|
? { enabled: true, domain: process.env.AUTH_COOKIE_DOMAIN.trim() }
|
||||||
|
: { enabled: false },
|
||||||
database: {
|
database: {
|
||||||
generateId: false
|
generateId: false
|
||||||
}
|
}
|
||||||
@@ -113,14 +134,7 @@ export const auth: ReturnType<typeof betterAuth> = betterAuth({
|
|||||||
// OIDC providers are registered dynamically per organization
|
// OIDC providers are registered dynamically per organization
|
||||||
// via the admin API (auth.api.registerSSOProvider)
|
// via the admin API (auth.api.registerSSOProvider)
|
||||||
}),
|
}),
|
||||||
// Lets OAuth/SSO callbacks land on the canonical production URL
|
...buildOAuthProxyPlugin(),
|
||||||
// (api.classroomio.com) while still completing the flow on the tenant
|
|
||||||
// host the user signed in from (<org>.classroomio.school or a BYOD
|
|
||||||
// domain). Without this, the state cookie set on the tenant host isn't
|
|
||||||
// sent to the api host on the Google redirect → state_security_mismatch.
|
|
||||||
oAuthProxy({
|
|
||||||
productionURL: CONSTANTS.BASE_URL
|
|
||||||
}),
|
|
||||||
loginLink(),
|
loginLink(),
|
||||||
tokenExchange(),
|
tokenExchange(),
|
||||||
// Attaches the user's org memberships ({ [orgId]: roleId }) to the session
|
// Attaches the user's org memberships ({ [orgId]: roleId }) to the session
|
||||||
|
|||||||
Reference in New Issue
Block a user