Security Enhancement: - Server-side validation of email status before allowing verification - Only allow verifying emails in VerificationPending state - Show "Action not possible" message for invalid states - Extract and validate share-id on server side - Return 404 for invalid share-ids or missing tenant emails Implementation: - Convert page.tsx to async server component - Fetch location and check tenantEmailStatus - Pass isPending prop to client component - Add bilingual "not-allowed" translations (same as unsubscribe page) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
123 lines
4.1 KiB
TypeScript
123 lines
4.1 KiB
TypeScript
'use client';
|
|
|
|
import { useTranslations } from 'next-intl';
|
|
import { useState } from 'react';
|
|
import { verifyTenantEmail } from '@/app/lib/actions/emailActions';
|
|
import { CheckCircleIcon } from '@heroicons/react/24/outline';
|
|
|
|
interface EmailVerifyPageProps {
|
|
shareId: string;
|
|
isPending: boolean;
|
|
}
|
|
|
|
export default function EmailVerifyPage({ shareId, isPending }: EmailVerifyPageProps) {
|
|
const t = useTranslations('email-verify-page');
|
|
const [isVerifying, setIsVerifying] = useState(false);
|
|
const [isVerified, setIsVerified] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const handleVerify = async () => {
|
|
setIsVerifying(true);
|
|
setError(null);
|
|
|
|
try {
|
|
const result = await verifyTenantEmail(shareId);
|
|
|
|
if (result.success) {
|
|
setIsVerified(true);
|
|
} else {
|
|
setError(result.message || t('error.unknown'));
|
|
}
|
|
} catch (err) {
|
|
setError(t('error.unknown'));
|
|
} finally {
|
|
setIsVerifying(false);
|
|
}
|
|
};
|
|
|
|
if (isVerified) {
|
|
return (
|
|
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
|
|
<div className="card-body">
|
|
<div className="flex justify-center mb-4">
|
|
<CheckCircleIcon className="h-16 w-16 text-success" />
|
|
</div>
|
|
<h2 className="card-title text-center justify-center text-success">
|
|
{t('success.title')}
|
|
</h2>
|
|
<p className="text-center">{t('success.message')}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
|
|
<div className="card-body">
|
|
<h2 className="card-title text-error">{t('error.title')}</h2>
|
|
<p>{error}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!isPending) {
|
|
return (
|
|
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
|
|
<div className="card-body">
|
|
<h2 className="card-title text-warning">{t('not-allowed.title')}</h2>
|
|
<p>{t('not-allowed.message')}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="card bg-base-100 shadow-xl max-w-2xl mx-auto mt-8">
|
|
<div className="card-body">
|
|
<h2 className="card-title mb-3">{t('title')}</h2>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h3 className="font-semibold text-lg">{t('about.title')}</h3>
|
|
<p>{t('about.description')}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="font-semibold text-lg">{t('why.title')}</h3>
|
|
<p>{t('why.description')}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="font-semibold text-lg">{t('what-happens.title')}</h3>
|
|
<p>{t('what-happens.description')}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h3 className="font-semibold text-lg">{t('opt-out.title')}</h3>
|
|
<p>{t('opt-out.description')}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="card-actions justify-center mt-6">
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={handleVerify}
|
|
disabled={isVerifying}
|
|
>
|
|
{isVerifying ? (
|
|
<>
|
|
<span className="loading loading-spinner"></span>
|
|
{t('button.verifying')}
|
|
</>
|
|
) : (
|
|
t('button.verify')
|
|
)}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|