From 5d1602df7fbba8c1b11bb449c01432315a4d0ca0 Mon Sep 17 00:00:00 2001 From: Knee Cola Date: Mon, 29 Dec 2025 20:33:16 +0100 Subject: [PATCH] feat: add email verification check to unsubscribe page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../unsubscribe/[id]/EmailUnsubscribePage.tsx | 14 +++++++- .../[locale]/email/unsubscribe/[id]/page.tsx | 34 ++++++++++++++++++- web-app/messages/en.json | 4 +++ web-app/messages/hr.json | 4 +++ 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/web-app/app/[locale]/email/unsubscribe/[id]/EmailUnsubscribePage.tsx b/web-app/app/[locale]/email/unsubscribe/[id]/EmailUnsubscribePage.tsx index ac3eb0b..a57dcf1 100644 --- a/web-app/app/[locale]/email/unsubscribe/[id]/EmailUnsubscribePage.tsx +++ b/web-app/app/[locale]/email/unsubscribe/[id]/EmailUnsubscribePage.tsx @@ -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 ( +
+
+

{t('not-allowed.title')}

+

{t('not-allowed.message')}

+
+
+ ); + } + return (
diff --git a/web-app/app/[locale]/email/unsubscribe/[id]/page.tsx b/web-app/app/[locale]/email/unsubscribe/[id]/page.tsx index 4300d9e..5c7c5a2 100644 --- a/web-app/app/[locale]/email/unsubscribe/[id]/page.tsx +++ b/web-app/app/[locale]/email/unsubscribe/[id]/page.tsx @@ -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("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 (
Loading...
}> - + ); diff --git a/web-app/messages/en.json b/web-app/messages/en.json index a4fb54c..31a338d 100644 --- a/web-app/messages/en.json +++ b/web-app/messages/en.json @@ -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": { diff --git a/web-app/messages/hr.json b/web-app/messages/hr.json index 4fc3926..dde799a 100644 --- a/web-app/messages/hr.json +++ b/web-app/messages/hr.json @@ -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": {