Security Release v0.2.6: Email Verification Bypass Vulnerabilities Fixed
Critical security fixes addressing email verification bypass vulnerabilities: Security Issues Fixed: - Email verification status manipulation through client-side profile updates - Base64 token forgery allowing arbitrary email verification - DOM manipulation bypasses of email verification modal Backend Security Enhancements: - Secure verification tokens table with cryptographic token generation - RLS policies preventing direct email verification status changes - Server-side validation functions with expiration and ownership checks - Database triggers blocking unauthorized verification status updates - IP address logging and audit trails for security monitoring Frontend Security Protections: - Multi-layered DOM protection preventing UI manipulation bypasses - Continuous monitoring and re-application of interaction restrictions - CSS-based backup protection even if JavaScript is disabled - Mutation observer to protect dynamically added elements - Enhanced CSP configuration for Senja widget support API Security Updates: - Secure token-based verification replacing insecure Base64 method - Comprehensive error handling with security-focused user messages - Token validation with signature verification and expiration checks - Legacy verification method blocking with security warnings - Enhanced verification email generation with secure tokens Files Modified: - Database: New email_verification_tokens table and security functions - API: Secure verification endpoints with token validation - Frontend: DOM protection and verification modal enhancements - CSP: Updated Content Security Policy configuration - Blog: Security advisory for CVE-2025-65675 disclosure This release patches critical authentication bypass vulnerabilities and implements defense-in-depth security measures across the entire platform. Security researchers and users should upgrade immediately.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const cronSecret = process.env.CRON_SECRET;
|
||||
|
||||
if (!cronSecret || authHeader !== `Bearer ${cronSecret}`) {
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized'
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
try {
|
||||
// Clean up expired tokens
|
||||
const { data: cleanupResult, error } = await supabase.rpc(
|
||||
'cleanup_expired_verification_tokens'
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error('Token cleanup failed:', error);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: error.message
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Token cleanup completed:', {
|
||||
deletedTokens: cleanupResult,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
deletedTokens: cleanupResult,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Unexpected error during cleanup:', error);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unexpected error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
export async function GET() {
|
||||
return json({
|
||||
service: 'email-verification-cleanup',
|
||||
status: 'active',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
|
||||
export async function GET({ url, request }) {
|
||||
// Simple auth check - in production this should be more robust
|
||||
const authHeader = request.headers.get('authorization');
|
||||
const adminSecret = process.env.ADMIN_SECRET;
|
||||
|
||||
if (!adminSecret || authHeader !== `Bearer ${adminSecret}`) {
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unauthorized'
|
||||
},
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = getServerSupabase();
|
||||
|
||||
try {
|
||||
// Get verification token statistics
|
||||
const { data: tokenStats, error: tokenError } = await supabase
|
||||
.from('email_verification_tokens')
|
||||
.select('id, created_at, expires_at, used_at, created_by_ip, used_by_ip, email')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(100);
|
||||
|
||||
if (tokenError) {
|
||||
console.error('Failed to fetch token stats:', tokenError);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Database error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate stats
|
||||
const now = new Date();
|
||||
const last24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000);
|
||||
|
||||
const recent = tokenStats.filter((token) => new Date(token.created_at) > last24Hours);
|
||||
const used = tokenStats.filter((token) => token.used_at);
|
||||
const expired = tokenStats.filter(
|
||||
(token) => new Date(token.expires_at) < now && !token.used_at
|
||||
);
|
||||
|
||||
// Count unique IPs for anomaly detection
|
||||
const uniqueCreationIPs = new Set(tokenStats.map((t) => t.created_by_ip)).size;
|
||||
const uniqueUsageIPs = new Set(tokenStats.filter((t) => t.used_by_ip).map((t) => t.used_by_ip))
|
||||
.size;
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
stats: {
|
||||
total_tokens: tokenStats.length,
|
||||
recent_24h: recent.length,
|
||||
used_tokens: used.length,
|
||||
expired_unused: expired.length,
|
||||
unique_creation_ips: uniqueCreationIPs,
|
||||
unique_usage_ips: uniqueUsageIPs,
|
||||
success_rate:
|
||||
tokenStats.length > 0 ? ((used.length / tokenStats.length) * 100).toFixed(1) : 0
|
||||
},
|
||||
recent_activity: recent.slice(0, 10).map((token) => ({
|
||||
created_at: token.created_at,
|
||||
email: token.email.replace(/(.{2}).*@/, '$1***@'), // Mask email for privacy
|
||||
used: !!token.used_at,
|
||||
expired: new Date(token.expires_at) < now,
|
||||
ip: token.created_by_ip
|
||||
}))
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Security monitoring error:', error);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
error: 'Unexpected error'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,65 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import sendEmail from '$mail/sendEmail';
|
||||
import { getServerSupabase } from '$lib/utils/functions/supabase.server';
|
||||
|
||||
export async function POST({ fetch, request }) {
|
||||
const { to, profileId, orgSiteName, fullname } = await request.json();
|
||||
const { to, profileId, fullname } = await request.json();
|
||||
console.log('/POST api/email/verify_email', to, profileId);
|
||||
|
||||
if (!to || !profileId) {
|
||||
return json({ success: false, message: 'Name and To are required fields' }, { status: 400 });
|
||||
return json(
|
||||
{ success: false, message: 'Email and Profile ID are required fields' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const supabase = getServerSupabase();
|
||||
const { data: profile, error: profileError } = await supabase
|
||||
.from('profile')
|
||||
.select('id, email, fullname')
|
||||
.eq('id', profileId)
|
||||
.single();
|
||||
|
||||
if (profileError || !profile) {
|
||||
console.error('Profile verification failed:', profileError);
|
||||
return json({ success: false, message: 'Invalid profile' }, { status: 403 });
|
||||
}
|
||||
|
||||
if (profile.email !== to) {
|
||||
console.error('Email mismatch:', { profileEmail: profile.email, requestedEmail: to });
|
||||
return json({ success: false, message: 'Email mismatch' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Get client IP for audit trail
|
||||
const clientIP =
|
||||
request.headers.get('cf-connecting-ip') ||
|
||||
request.headers.get('x-forwarded-for') ||
|
||||
request.headers.get('x-real-ip') ||
|
||||
'unknown';
|
||||
|
||||
// Generate secure verification token
|
||||
const { data: tokenResult, error: tokenError } = await supabase.rpc(
|
||||
'create_email_verification_token',
|
||||
{
|
||||
profile_id_input: profileId,
|
||||
email_input: to,
|
||||
creator_ip: clientIP
|
||||
}
|
||||
);
|
||||
|
||||
if (tokenError || !tokenResult?.success) {
|
||||
console.error('Token generation failed:', tokenError, tokenResult);
|
||||
return json(
|
||||
{
|
||||
success: false,
|
||||
message: tokenResult?.message || 'Failed to generate verification token'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
const origin = request.headers.get('origin');
|
||||
const verificationData = JSON.stringify({
|
||||
profileId,
|
||||
orgSiteName
|
||||
});
|
||||
|
||||
const verificationLink = `${origin}/api/verify?data=${encodeURIComponent(
|
||||
btoa(verificationData)
|
||||
)}`;
|
||||
const verificationLink = `${origin}/api/verify?token=${encodeURIComponent(tokenResult.token)}`;
|
||||
|
||||
const emailData = [
|
||||
{
|
||||
@@ -26,19 +68,40 @@ export async function POST({ fetch, request }) {
|
||||
subject: 'Action Required: Confirm your email',
|
||||
content: `
|
||||
<p><strong>Hi ${fullname} 👋</strong></p>
|
||||
<p>Welcome to ClassroomIO, new friend! In order to get your account ready for usage, we need to verify your email. </p>
|
||||
<p>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 <strong>Verify</strong> button below
|
||||
</p>
|
||||
<p>Welcome to ClassroomIO, new friend! In order to get your account ready for usage, we need to verify your email address. </p>
|
||||
<p>We do this to ensure the security of your account and prevent unauthorized access. To complete your registration, please click the <strong>Verify</strong> button below:</p>
|
||||
<div>
|
||||
<a class="button" href="${verificationLink}">Verify</a>
|
||||
<a class="button" href="${verificationLink}">Verify Email Address</a>
|
||||
</div>
|
||||
<p><strong>Security Notice:</strong></p>
|
||||
<ul>
|
||||
<li>This verification link will expire in 1 hour</li>
|
||||
<li>This link can only be used once</li>
|
||||
<li>If you didn't request this verification, please ignore this email</li>
|
||||
</ul>
|
||||
<p>If you're having trouble clicking the button, copy and paste the following link into your browser:</p>
|
||||
<p style="word-break: break-all;">${verificationLink}</p>
|
||||
`
|
||||
}
|
||||
];
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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]
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
|
||||
let errorType = '';
|
||||
let errorMessage = '';
|
||||
let errorTitle = '';
|
||||
let showRetry = false;
|
||||
|
||||
onMount(() => {
|
||||
errorType = $page.url.searchParams.get('error') || 'unknown';
|
||||
|
||||
switch (errorType) {
|
||||
case 'invalid_token':
|
||||
errorTitle = $t('email_verification.errors.invalid_token.title');
|
||||
errorMessage = $t('email_verification.errors.invalid_token.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'token_already_used':
|
||||
errorTitle = $t('email_verification.errors.token_used.title');
|
||||
errorMessage = $t('email_verification.errors.token_used.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'token_expired':
|
||||
errorTitle = $t('email_verification.errors.token_expired.title');
|
||||
errorMessage = $t('email_verification.errors.token_expired.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'email_mismatch':
|
||||
errorTitle = $t('email_verification.errors.email_mismatch.title');
|
||||
errorMessage = $t('email_verification.errors.email_mismatch.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'legacy_method_blocked':
|
||||
errorTitle = $t('email_verification.errors.legacy_blocked.title');
|
||||
errorMessage = $t('email_verification.errors.legacy_blocked.message');
|
||||
showRetry = true;
|
||||
break;
|
||||
case 'database_error':
|
||||
errorTitle = $t('email_verification.errors.database.title');
|
||||
errorMessage = $t('email_verification.errors.database.message');
|
||||
showRetry = false;
|
||||
break;
|
||||
default:
|
||||
errorTitle = $t('email_verification.errors.unknown.title');
|
||||
errorMessage = $t('email_verification.errors.unknown.message');
|
||||
showRetry = true;
|
||||
}
|
||||
});
|
||||
|
||||
function handleRetry() {
|
||||
goto('/dashboard');
|
||||
}
|
||||
|
||||
function handleSupport() {
|
||||
window.open(
|
||||
'mailto:[email protected]?subject=Email Verification Issue&body=Error Type: ' + errorType,
|
||||
'_blank'
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Email Verification Error - ClassroomIO</title>
|
||||
<meta name="description" content="There was an issue verifying your email address." />
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="flex min-h-screen w-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8 dark:bg-gray-900"
|
||||
>
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div class="text-center">
|
||||
<!-- Error Icon -->
|
||||
<svg
|
||||
class="mx-auto h-12 w-12 text-red-500"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<h2 class="mt-6 text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{errorTitle}
|
||||
</h2>
|
||||
|
||||
<p class="mt-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
{errorMessage}
|
||||
</p>
|
||||
|
||||
{#if errorType === 'legacy_method_blocked'}
|
||||
<div
|
||||
class="mt-4 rounded-md border border-yellow-200 bg-yellow-50 p-4 dark:border-yellow-800 dark:bg-yellow-900/20"
|
||||
>
|
||||
<div class="flex">
|
||||
<svg
|
||||
class="h-5 w-5 flex-shrink-0 text-yellow-400"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-yellow-800 dark:text-yellow-200">
|
||||
{$t('email_verification.security_notice.title')}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-yellow-700 dark:text-yellow-300">
|
||||
{$t('email_verification.security_notice.message')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex w-full flex-col justify-center gap-4 md:flex-row">
|
||||
<PrimaryButton
|
||||
onClick={handleSupport}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('email_verification.actions.contact_support')}
|
||||
/>
|
||||
<PrimaryButton
|
||||
onClick={() => goto('/')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('email_verification.actions.back_to_dashboard')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Security Information -->
|
||||
<div class="mt-8 text-center text-xs text-gray-500 dark:text-gray-400">
|
||||
<p>{$t('email_verification.security_footer')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user