fix(dashboard): keep unverified users on enroll page after join

After email login, auto-enroll was redirecting to /lms or the admin
/courses/:id/lessons route. Unverified users cannot use the LMS yet
(VerifyEmailModal on app routes), which felt like a permission error.

Enroll without verification, but stay on the enroll page with a verify
prompt instead of redirecting until email is confirmed. Also fix custom
domain org lookup to use hostname without backend port.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-07-10 04:32:10 +00:00
co-authored by Rotimi Best
parent be0085f090
commit f262a092ed
14 changed files with 105 additions and 21 deletions
@@ -54,7 +54,7 @@ export async function getOrgSiteInfo(url: URL, cookies: Cookies): Promise<OrgSit
if (isURLCustomDomain(url)) {
console.log('it is custom domain');
const apiKeyHeaders = getApiKeyHeaders();
const orgs = await getOrgsByCustomDomain(url.host, true, apiKeyHeaders);
const orgs = await getOrgsByCustomDomain(url.hostname, true, apiKeyHeaders);
if (!orgs || orgs.length === 0) {
return response;
@@ -892,7 +892,9 @@
"enroll_page": {
"requires_payment_or_invite": "Du skal betale for dette kursus eller blive inviteret for at få adgang.",
"back_to_course": "Tilbage til kurset",
"join_course": "Deltag i kursus"
"join_course": "Deltag i kursus",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"pricing_section": {
"free": "Gratis",
@@ -892,7 +892,9 @@
"enroll_page": {
"requires_payment_or_invite": "Sie müssen für diesen Kurs bezahlen oder eingeladen werden, um Zugang zu erhalten.",
"back_to_course": "Zurück zum Kurs",
"join_course": "Nehmen Sie am Kurs teil"
"join_course": "Nehmen Sie am Kurs teil",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"pricing_section": {
"free": "Kostenlos",
@@ -3371,7 +3371,9 @@
"enroll_page": {
"requires_payment_or_invite": "You need to pay for this course or be invited to get access.",
"back_to_course": "Back to course",
"join_course": "Join Course"
"join_course": "Join Course",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"course_content": "Course content",
"modules": "modules",
@@ -892,7 +892,9 @@
"enroll_page": {
"requires_payment_or_invite": "Debes pagar por este curso o ser invitado a obtener acceso.",
"back_to_course": "volver al curso",
"join_course": "Unirse al curso"
"join_course": "Unirse al curso",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"pricing_section": {
"free": "Gratis",
@@ -892,7 +892,9 @@
"enroll_page": {
"requires_payment_or_invite": "Vous devez payer pour ce cours ou être invité pour y accéder.",
"back_to_course": "Retour au cours",
"join_course": "Rejoindre le cours"
"join_course": "Rejoindre le cours",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"pricing_section": {
"free": "Gratuit",
@@ -892,7 +892,9 @@
"enroll_page": {
"requires_payment_or_invite": "आपको इस पाठ्यक्रम के लिए भुगतान करना होगा या प्रवेश पाने के लिए आमंत्रित किया जाना होगा।",
"back_to_course": "पाठ्यक्रम पर वापस जाएँ",
"join_course": "पाठ्यक्रम में शामिल हों"
"join_course": "पाठ्यक्रम में शामिल हों",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"pricing_section": {
"free": "निःशुल्क",
@@ -892,7 +892,9 @@
"enroll_page": {
"requires_payment_or_invite": "Aby uzyskać dostęp, musisz zapłacić za ten kurs lub otrzymać zaproszenie.",
"back_to_course": "Powrót na kurs",
"join_course": "Dołącz do kursu"
"join_course": "Dołącz do kursu",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"pricing_section": {
"free": "Bezpłatny",
@@ -892,7 +892,9 @@
"enroll_page": {
"requires_payment_or_invite": "Você precisa pagar por este curso ou ser convidado para obter acesso.",
"back_to_course": "De volta ao curso",
"join_course": "Participe do curso"
"join_course": "Participe do curso",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"pricing_section": {
"free": "Grátis",
@@ -892,7 +892,11 @@
"enroll_page": {
"requires_payment_or_invite": "Вам нужно заплатить за этот курс или получить приглашение для получения доступа.",
"back_to_course": "Вернуться к курсу",
"join_course": "Присоединиться к курсу"
"join_course": "Присоединиться к курсу",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area.",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"pricing_section": {
"free": "Бесплатно",
@@ -892,7 +892,9 @@
"enroll_page": {
"requires_payment_or_invite": "Bạn cần phải trả tiền cho khóa học này hoặc được mời để có quyền truy cập.",
"back_to_course": "Quay lại khóa học",
"join_course": "Tham gia khóa học"
"join_course": "Tham gia khóa học",
"enrolled_success": "You're enrolled in this course.",
"enrolled_verify_email": "You're enrolled! Verify your email to access this course in your learning area."
},
"pricing_section": {
"free": "miễn phí",
@@ -1,11 +1,18 @@
import { PUBLIC_ORG_SITE_LIGHT_MODE_ROUTES } from '$lib/utils/constants/routes';
import { error } from '@sveltejs/kit';
export const load = async ({ parent }) => {
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');
}
@@ -56,9 +56,12 @@ export const load = async ({ params, url }) => {
}
const courseResult = await safeServerApi<GetCourseBySlugSuccess>(() =>
classroomio.course.slug[':slug'].$get({
param: { slug }
})
classroomio.course.slug[':slug'].$get(
{
param: { slug }
},
getApiKeyHeaders()
)
);
if (!courseResult.ok || !courseResult.body.data) {
@@ -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 @@
<AuthUI
isLogin={false}
{handleSubmit}
isLoading={loading || !sessionReady || (isLoggedIn && canJoinCourse)}
isLoading={loading || !sessionReady || (isLoggedIn && canJoinCourse && enrollmentInFlight)}
showOnlyContent={true}
showLogo={true}
>
<div class="mt-0 w-full">
<h3 class="mt-0 mb-4 text-center text-lg font-medium dark:text-white">{data.course?.title}</h3>
<p class="text-center text-sm font-light dark:text-white">{data.course?.description}</p>
{#if data.requiresPaymentOrInvite}
{#if enrolledPendingVerification}
<p class="ui:text-primary mt-3 text-center text-sm font-medium">
{$t('course.navItem.landing_page.enroll_page.enrolled_success')}
</p>
<p class="ui:text-muted-foreground mt-2 text-center text-sm">
{$t('course.navItem.landing_page.enroll_page.enrolled_verify_email')}
</p>
<div class="mt-4 flex justify-center">
<Button type="button" variant="outline" onclick={sendVerificationEmail}>
{$t('verify_email_modal.resend')}
</Button>
</div>
{:else if data.requiresPaymentOrInvite}
<p class="ui:text-muted-foreground mt-3 text-center text-sm">
{$t('course.navItem.landing_page.enroll_page.requires_payment_or_invite')}
</p>
@@ -189,9 +241,9 @@
{/if}
</div>
{#if !isLoggedIn}
{#if !enrolledPendingVerification && (!isLoggedIn || (isLoggedIn && canJoinCourse && !enrollmentInFlight))}
<div class="my-4 flex w-full items-center justify-center">
<Button type="submit" disabled={!canJoinCourse || loading || !sessionReady} {loading}>
<Button type="submit" disabled={!canJoinCourse || loading || !sessionReady || enrollmentInFlight} {loading}>
{$t('course.navItem.landing_page.enroll_page.join_course')}
</Button>
</div>