From 5320f1eb44b958c52a1fe59e827a07010c8dd447 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Fri, 19 Jun 2026 14:50:37 +0100 Subject: [PATCH] fix(dashboard): use verified custom domain for view-as-student handoff (#677) * fix(dashboard): use verified custom domain for view-as-student handoff Prefer the org's verified custom domain over window.location.origin when resolving the public tenant host. This prevents admin URLs like app.example.com from being used for the student login-link handoff when the org's public site is example.com. Co-authored-by: Rotimi Best * fix(dashboard): keep self-hosted org origin on window.location Self-hosted deployments serve admin and LMS from the same URL, so getOrgPublicOrigin should always return window.location.origin there rather than preferring a configured custom domain. Co-authored-by: Rotimi Best --------- Co-authored-by: Cursor Agent Co-authored-by: Rotimi Best --- .../features/course/utils/course-preview.ts | 6 ++- apps/dashboard/src/lib/utils/store/org.ts | 37 +++++++++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/apps/dashboard/src/lib/features/course/utils/course-preview.ts b/apps/dashboard/src/lib/features/course/utils/course-preview.ts index a32a6f9e..023be090 100644 --- a/apps/dashboard/src/lib/features/course/utils/course-preview.ts +++ b/apps/dashboard/src/lib/features/course/utils/course-preview.ts @@ -1,7 +1,9 @@ +import { get } from 'svelte/store'; import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import { snackbar } from '$features/ui/snackbar/store'; import { accountApi } from '$features/account/api/account.svelte'; +import { currentOrg, getOrgPublicOrigin } from '$lib/utils/store/org'; interface OpenCoursePreviewOptions { courseId: string; @@ -21,7 +23,7 @@ export function getPublicCoursePageUrl(courseSlug: string, currentOrgDomain = '' } const trimmedDomain = currentOrgDomain?.trim(); - const origin = trimmedDomain || window.location.origin; + const origin = trimmedDomain || getOrgPublicOrigin(get(currentOrg)); return new URL(resolve(`/course/${courseSlug}`, {}), origin).toString(); } @@ -73,7 +75,7 @@ export async function viewAsStudent({ courseId, currentOrgDomain = '' }: ViewAsS return false; } - const origin = currentOrgDomain?.trim() || window.location.origin; + const origin = currentOrgDomain?.trim() || getOrgPublicOrigin(get(currentOrg)); const loginLinkUrl = new URL('/api/auth/login-link', origin); loginLinkUrl.searchParams.set('token', token); loginLinkUrl.searchParams.set('redirect', `/courses/${courseId}/lessons?next=true`); diff --git a/apps/dashboard/src/lib/utils/store/org.ts b/apps/dashboard/src/lib/utils/store/org.ts index 2c5f758d..2fdf0349 100644 --- a/apps/dashboard/src/lib/utils/store/org.ts +++ b/apps/dashboard/src/lib/utils/store/org.ts @@ -77,19 +77,34 @@ export const currentOrgPath = derived(currentOrg, ($currentOrg) => $currentOrg.siteName ? `/org/${$currentOrg.siteName}` : '#' ); -export const currentOrgDomain = derived(currentOrg, ($currentOrg) => { - if (PUBLIC_IS_SELFHOSTED === 'true') return window.location.origin; +type OrgPublicOrigin = Pick; - const browserOrigin = dev && browser && window.location.origin; +/** + * Public origin for an org's tenant site (student LMS, public course pages, login-link handoff). + * Self-hosted uses the current deployment URL. On cloud, verified custom domains win over the + * admin host so `app.classroomio.com` still hands off to the org's `example.com` tenant site. + */ +export function getOrgPublicOrigin(org: OrgPublicOrigin): string { + if (PUBLIC_IS_SELFHOSTED === 'true') { + return browser ? window.location.origin : ''; + } - return browserOrigin - ? browserOrigin - : $currentOrg.customDomain && $currentOrg.isCustomDomainVerified - ? `https://${$currentOrg.customDomain}` - : $currentOrg.siteName - ? `https://${$currentOrg.siteName}.${TENANT_ROOT_DOMAIN}` - : ''; -}); + if (org.customDomain && org.isCustomDomainVerified) { + return `https://${org.customDomain}`; + } + + if (dev && browser) { + return window.location.origin; + } + + if (org.siteName) { + return `https://${org.siteName}.${TENANT_ROOT_DOMAIN}`; + } + + return browser ? window.location.origin : ''; +} + +export const currentOrgDomain = derived(currentOrg, ($currentOrg) => getOrgPublicOrigin($currentOrg)); export const isFreePlan = derived(currentOrg, ($currentOrg) => { if (!$currentOrg.id || PUBLIC_IS_SELFHOSTED === 'true') return false;