feat: add email verification check to unsubscribe page

Security Enhancement:
- Server-side validation of email status before allowing unsubscribe
- Only allow unsubscribing from verified emails
- Show "Action Not Allowed" message for unverified/unsubscribed emails
- 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 isVerified prop to client component
- Add bilingual "not-allowed" translations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Knee Cola
2025-12-29 20:33:16 +01:00
parent bc7b28e6e9
commit 5d1602df7f
4 changed files with 54 additions and 2 deletions

View File

@@ -7,9 +7,10 @@ import { CheckCircleIcon } from '@heroicons/react/24/outline';
interface EmailUnsubscribePageProps {
shareId: string;
isVerified: boolean;
}
export default function EmailUnsubscribePage({ shareId }: EmailUnsubscribePageProps) {
export default function EmailUnsubscribePage({ shareId, isVerified }: EmailUnsubscribePageProps) {
const t = useTranslations('email-unsubscribe-page');
const [isUnsubscribing, setIsUnsubscribing] = useState(false);
const [isUnsubscribed, setIsUnsubscribed] = useState(false);
@@ -61,6 +62,17 @@ export default function EmailUnsubscribePage({ shareId }: EmailUnsubscribePagePr
);
}
if (!isVerified) {
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">

View File

@@ -1,12 +1,44 @@
import { Suspense } from 'react';
import EmailUnsubscribePage from './EmailUnsubscribePage';
import { Main } from '@/app/ui/Main';
import { getDbClient } from '@/app/lib/dbClient';
import { BillingLocation, EmailStatus } from '@/app/lib/db-types';
import { extractShareId, validateShareChecksum } from '@/app/lib/shareChecksum';
import { notFound } from 'next/navigation';
export default async function Page({ params: { id } }: { params: { id: string } }) {
// Extract and validate share ID
const extracted = extractShareId(id);
if (!extracted) {
notFound();
}
const { locationId, checksum } = extracted;
// Validate checksum
if (!validateShareChecksum(locationId, checksum)) {
notFound();
}
// Fetch location to check email status
const dbClient = await getDbClient();
const location = await dbClient.collection<BillingLocation>("lokacije")
.findOne(
{ _id: locationId },
{ projection: { tenantEmail: 1, tenantEmailStatus: 1 } }
);
if (!location || !location.tenantEmail) {
notFound();
}
// Check if email is verified
const isVerified = location.tenantEmailStatus === EmailStatus.Verified;
return (
<Main>
<Suspense fallback={<div className="text-center p-8">Loading...</div>}>
<EmailUnsubscribePage shareId={id} />
<EmailUnsubscribePage shareId={id} isVerified={isVerified} />
</Suspense>
</Main>
);

View File

@@ -482,6 +482,10 @@
"error": {
"title": "Unsubscribe Failed",
"unknown": "An error occurred while unsubscribing. Please try again or contact your landlord."
},
"not-allowed": {
"title": "Action Not Allowed",
"message": "You can only unsubscribe from verified email addresses. This email address has not been verified yet or has already been unsubscribed."
}
},
"privacy-policy-page": {

View File

@@ -479,6 +479,10 @@
"error": {
"title": "Odjava Nije Uspjela",
"unknown": "Došlo je do greške prilikom odjave. Molimo pokušajte ponovno ili kontaktirajte vašeg vlasnika nekretnine."
},
"not-allowed": {
"title": "Akcija Nije Dopuštena",
"message": "Možete se odjaviti samo s potvrđenih email adresa. Ova email adresa još nije potvrđena ili je već odjavljena."
}
},
"privacy-policy-page": {