Hi ${fullname} 👋
-Welcome to ClassroomIO, new friend! In order to get your account ready for usage, we need to verify your email.
-We do this to make sure we don't get fake user emails in our signup. To get the best out of our product, we'll need you to verify your email by clicking the Verify button below -
+Welcome to ClassroomIO, new friend! In order to get your account ready for usage, we need to verify your email address.
+We do this to ensure the security of your account and prevent unauthorized access. To complete your registration, please click the Verify button below:
+Security Notice:
+If you're having trouble clicking the button, copy and paste the following link into your browser:
+${verificationLink}
` } ]; - await sendEmail(fetch)(emailData); + try { + await sendEmail(fetch)(emailData); + console.log('Verification email sent successfully to:', to); + } catch (emailError) { + console.error('Email sending failed:', emailError); + return json( + { + success: false, + message: 'Failed to send verification email' + }, + { status: 500 } + ); + } return json({ - success: true + success: true, + message: 'Verification email sent successfully', + expiresAt: tokenResult.expires_at }); } diff --git a/apps/dashboard/src/routes/api/verify/+server.js b/apps/dashboard/src/routes/api/verify/+server.js index ec008018..6f029104 100644 --- a/apps/dashboard/src/routes/api/verify/+server.js +++ b/apps/dashboard/src/routes/api/verify/+server.js @@ -1,29 +1,95 @@ import { redirect, json } from '@sveltejs/kit'; import { getServerSupabase } from '$lib/utils/functions/supabase.server'; -export const GET = async ({ url }) => { +export const GET = async ({ url, getClientAddress }) => { const supabase = getServerSupabase(); - const data = url.searchParams.get('data'); - if (!data) return json({ message: 'data is not found' }, { status: 404 }); + const token = url.searchParams.get('token'); - const hashData = decodeURIComponent(data); - console.log('hashData', hashData); - - const decodedData = atob(hashData); - console.log('decodedData', decodedData); - - const { profileId, orgSiteName } = JSON.parse(decodedData); - - const { error } = await supabase - .from('profile') - .update({ is_email_verified: true, verified_at: new Date().toDateString() }) - .match({ id: profileId }); - - if (error) { - console.log('profile not found', error); - throw redirect(307, '/404'); + if (!token) { + console.error('No verification token provided'); + return json( + { + success: false, + error: 'MISSING_TOKEN', + message: 'Verification token is required' + }, + { status: 400 } + ); } - throw redirect(307, `/org/${orgSiteName}?welcomePopup=true`); + // Get client IP for audit trail + const clientIP = getClientAddress(); + + try { + // Use secure verification function + const { data: verificationResult, error: verificationError } = await supabase.rpc( + 'verify_email_with_token', + { + token_input: token, + user_ip: clientIP + } + ); + + if (verificationError) { + console.error('Database error during verification:', verificationError); + throw redirect(307, '/verify-email-error?error=database_error'); + } + + if (!verificationResult?.success) { + console.warn('Verification failed:', { + error: verificationResult?.error, + message: verificationResult?.message, + token: token?.substring(0, 8) + '...', + ip: clientIP + }); + + // Redirect to appropriate error page based on error type + const errorType = verificationResult?.error || 'unknown'; + throw redirect(307, `/verify-email-error?error=${errorType.toLowerCase()}`); + } + + // Success! Get the verified profile to redirect properly + const { data: profile, error: profileError } = await supabase + .from('profile') + .select('id') + .eq('id', verificationResult.profile_id) + .single(); + + if (profileError || !profile) { + console.error('Profile lookup failed after verification:', profileError); + throw redirect(307, '/verify-email-error?error=profile_not_found'); + } + + // Get organization for redirect + const { data: orgMember } = await supabase + .from('organizationmember') + .select('organization:organization_id(siteName)') + .eq('profile_id', profile.id) + .limit(1) + .single(); + + const orgSiteName = orgMember?.organization?.siteName; + + console.log('Email verification successful:', { + profileId: verificationResult.profile_id, + ip: clientIP, + timestamp: new Date().toISOString() + }); + + // Redirect to success page + if (orgSiteName) { + throw redirect(307, `/org/${orgSiteName}?welcomePopup=true`); + } else { + throw redirect(307, '/?emailVerified=true'); + } + } catch (error) { + // If it's already a redirect, re-throw it + if (error.status >= 300 && error.status < 400) { + throw error; + } + + console.error('Unexpected error during verification:', error); + throw redirect(307, '/verify-email-error?error=unexpected'); + } }; diff --git a/apps/dashboard/src/routes/csp-report/+server.ts b/apps/dashboard/src/routes/csp-report/+server.ts index 8d07b0d1..56a42f17 100644 --- a/apps/dashboard/src/routes/csp-report/+server.ts +++ b/apps/dashboard/src/routes/csp-report/+server.ts @@ -5,18 +5,18 @@ export const POST: RequestHandler = async ({ request }) => { const report = await request.json(); // Log the CSP violation for monitoring - console.warn('CSP Violation Report:', { - timestamp: new Date().toISOString(), - documentURI: report['document-uri'], - violatedDirective: report['violated-directive'], - blockedURI: report['blocked-uri'], - effectiveDirective: report['effective-directive'], - originalPolicy: report['original-policy'], - sourceFile: report['source-file'], - lineNumber: report['line-number'], - columnNumber: report['column-number'], - userAgent: request.headers.get('user-agent') - }); + // console.warn('CSP Violation Report:', { + // timestamp: new Date().toISOString(), + // documentURI: report['document-uri'], + // violatedDirective: report['violated-directive'], + // blockedURI: report['blocked-uri'], + // effectiveDirective: report['effective-directive'], + // originalPolicy: report['original-policy'], + // sourceFile: report['source-file'], + // lineNumber: report['line-number'], + // columnNumber: report['column-number'], + // userAgent: request.headers.get('user-agent') + // }); return new Response('OK', { status: 200 }); } catch (error) { diff --git a/apps/dashboard/src/routes/onboarding/+page.svelte b/apps/dashboard/src/routes/onboarding/+page.svelte index a0b758b1..03aecb46 100644 --- a/apps/dashboard/src/routes/onboarding/+page.svelte +++ b/apps/dashboard/src/routes/onboarding/+page.svelte @@ -108,14 +108,6 @@ fields.locale = newSelectedId; } - async function setMetaData() { - if (!env.PUBLIC_IP_REGISTRY_KEY) return; - - const response = await fetch(`https://api.ipregistry.co/?key=${env.PUBLIC_IP_REGISTRY_KEY}`); - const payload = await response.json(); - fields.metadata = payload; - } - function setOrgSiteName(orgName: string | undefined, isTouched: boolean) { if (!orgName || isTouched) return; @@ -193,8 +185,6 @@ // Submit filled // Set extra metadata based off location - await setMetaData(); - await supabase .from('profile') .update({ diff --git a/apps/dashboard/src/routes/signup/+page.svelte b/apps/dashboard/src/routes/signup/+page.svelte index f6993efa..6f12ff55 100644 --- a/apps/dashboard/src/routes/signup/+page.svelte +++ b/apps/dashboard/src/routes/signup/+page.svelte @@ -65,8 +65,6 @@ if (!$currentOrg.id) return; const [regexUsernameMatch] = [...(authUser.email?.matchAll(/(.*)@/g) || [])]; - const response = await fetch('https://api.ipregistry.co/?key=tryout'); - const metadata = await response.json(); const profileRes = await supabase .from('profile') @@ -74,8 +72,7 @@ id: authUser.id, username: regexUsernameMatch[1] + `${new Date().getTime()}`, fullname: regexUsernameMatch[1], - email: authUser.email, - metadata + email: authUser.email }) .select(); console.log('profileRes', profileRes); @@ -91,16 +88,14 @@ capturePosthogEvent('user_signed_up', { distinct_id: $profile.id || '', email: authUser.email, - username: regexUsernameMatch[1], - metadata + username: regexUsernameMatch[1] }); if ($globalStore.isOrgSite) { capturePosthogEvent('student_signed_up', { distinct_id: $profile.id || '', email: authUser.email, - username: regexUsernameMatch[1], - metadata + username: regexUsernameMatch[1] }); } diff --git a/apps/dashboard/src/routes/verify-email-error/+page.svelte b/apps/dashboard/src/routes/verify-email-error/+page.svelte new file mode 100644 index 00000000..26a1e674 --- /dev/null +++ b/apps/dashboard/src/routes/verify-email-error/+page.svelte @@ -0,0 +1,147 @@ + + ++ {errorMessage} +
+ + {#if errorType === 'legacy_method_blocked'} ++ {$t('email_verification.security_notice.message')} +
+{$t('email_verification.security_footer')}
+