diff --git a/apps/dashboard/src/lib/features/app/layout-setup.ts b/apps/dashboard/src/lib/features/app/layout-setup.ts index 1dc582a4..015ae09a 100644 --- a/apps/dashboard/src/lib/features/app/layout-setup.ts +++ b/apps/dashboard/src/lib/features/app/layout-setup.ts @@ -54,7 +54,7 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise { +function canResolveOrgFromCoursePath(pathname: string): boolean { + return PUBLIC_ORG_SITE_LIGHT_MODE_ROUTES.some((pattern) => new RegExp(pattern).test(pathname)); +} + +export const load = async ({ parent, url }) => { const { org, isOrgSite } = await parent(); // An org-site subdomain that doesn't resolve to a real org must 404 rather // than fall back to whatever org a requested course happens to belong to. - if (isOrgSite && !org) { + // Course landing/enroll pages can still resolve org context from the course + // slug when the host lookup fails (e.g. custom domain with a backend port). + if (isOrgSite && !org && !canResolveOrgFromCoursePath(url.pathname)) { throw error(404, 'Organization not found'); } diff --git a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts index 635f1b3a..c1a11a1f 100644 --- a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts +++ b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.server.ts @@ -56,9 +56,12 @@ export const load = async ({ params, url }) => { } const courseResult = await safeServerApi(() => - classroomio.course.slug[':slug'].$get({ - param: { slug } - }) + classroomio.course.slug[':slug'].$get( + { + param: { slug } + }, + getApiKeyHeaders() + ) ); if (!courseResult.ok || !courseResult.body.data) { diff --git a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte index 478f6912..8be53027 100644 --- a/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte +++ b/apps/dashboard/src/routes/(org-site)/course/[slug]/enroll/+page.svelte @@ -12,17 +12,46 @@ import { capturePosthogEvent } from '$lib/utils/services/posthog'; import { resolve } from '$app/paths'; import { authClient } from '$lib/utils/services/auth/client'; + import { appInitApi } from '$features/app/init.svelte'; let { data } = $props(); let loading = $state(false); let enrollmentInFlight = $state(false); let enrollmentAttemptKey = $state(''); + let enrolledPendingVerification = $state(false); const session = authClient.useSession(); const sessionReady = $derived(!$session.isPending && !$session.isRefetching); const sessionUser = $derived($session.data?.user ?? null); const isLoggedIn = $derived(sessionReady && Boolean(sessionUser)); + const isEmailVerified = $derived(Boolean($profile.isEmailVerified || sessionUser?.emailVerified)); + + function resolvePostEnrollRedirect(): string { + // Org-site enrollments are always students — the API's `/courses/:id/lessons` + // target is the admin course editor and 404s or blocks students on custom domains. + return '/lms'; + } + + async function sendVerificationEmail() { + const email = $profile.email || sessionUser?.email; + if (!email) { + snackbar.error('verify_email_modal.snackbar.error'); + return; + } + + try { + const callbackURL = new URL(page.url.href); + callbackURL.searchParams.set('trigger', 'app'); + + await authClient.sendVerificationEmail({ + email, + callbackURL: callbackURL.toString() + }); + } catch { + snackbar.error('verify_email_modal.snackbar.error'); + } + } const inviteStatus = $derived(data.invite?.status ?? 'INVALID'); const hasActiveInvite = $derived(Boolean(data.invite) && inviteStatus === 'ACTIVE'); @@ -74,6 +103,7 @@ const result = await courseApi.enroll(data.course.id, body); if (!result?.data) { + snackbar.error('snackbar.invite.failed_join'); return; } @@ -87,8 +117,14 @@ already_joined: result.data.alreadyJoined }); + if (!isEmailVerified) { + enrolledPendingVerification = true; + void sendVerificationEmail(); + return; + } + navigatingAway = true; - window.location.href = result.data.redirectTo || '/lms'; + window.location.href = resolvePostEnrollRedirect(); } finally { enrollmentInFlight = false; if (!navigatingAway) { @@ -133,6 +169,10 @@ return; } + if (!appInitApi.isInitializedAndReady) { + return; + } + const attemptKey = `${sessionUser?.id ?? ''}:${data.course?.id ?? ''}`; if (enrollmentAttemptKey === attemptKey) { return; @@ -167,14 +207,26 @@

{data.course?.title}

{data.course?.description}

- {#if data.requiresPaymentOrInvite} + {#if enrolledPendingVerification} +

+ {$t('course.navItem.landing_page.enroll_page.enrolled_success')} +

+

+ {$t('course.navItem.landing_page.enroll_page.enrolled_verify_email')} +

+
+ +
+ {:else if data.requiresPaymentOrInvite}

{$t('course.navItem.landing_page.enroll_page.requires_payment_or_invite')}

@@ -189,9 +241,9 @@ {/if}
- {#if !isLoggedIn} + {#if !enrolledPendingVerification && (!isLoggedIn || (isLoggedIn && canJoinCourse && !enrollmentInFlight))}
-