From db9c57472de1d767eee35c95f01b2f5f1acd55d8 Mon Sep 17 00:00:00 2001 From: Knee Cola Date: Mon, 29 Dec 2025 20:54:14 +0100 Subject: [PATCH] feat: add email status check to verify 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 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 --- .../email/verify/[id]/EmailVerifyPage.tsx | 14 +++++++- .../app/[locale]/email/verify/[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/verify/[id]/EmailVerifyPage.tsx b/web-app/app/[locale]/email/verify/[id]/EmailVerifyPage.tsx index 48cc4a7..a48de4a 100644 --- a/web-app/app/[locale]/email/verify/[id]/EmailVerifyPage.tsx +++ b/web-app/app/[locale]/email/verify/[id]/EmailVerifyPage.tsx @@ -7,9 +7,10 @@ import { CheckCircleIcon } from '@heroicons/react/24/outline'; interface EmailVerifyPageProps { shareId: string; + isPending: boolean; } -export default function EmailVerifyPage({ shareId }: EmailVerifyPageProps) { +export default function EmailVerifyPage({ shareId, isPending }: EmailVerifyPageProps) { const t = useTranslations('email-verify-page'); const [isVerifying, setIsVerifying] = useState(false); const [isVerified, setIsVerified] = useState(false); @@ -61,6 +62,17 @@ export default function EmailVerifyPage({ shareId }: EmailVerifyPageProps) { ); } + if (!isPending) { + return ( +
+
+

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

+

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

+
+
+ ); + } + return (
diff --git a/web-app/app/[locale]/email/verify/[id]/page.tsx b/web-app/app/[locale]/email/verify/[id]/page.tsx index e7d4360..3b4cc9d 100644 --- a/web-app/app/[locale]/email/verify/[id]/page.tsx +++ b/web-app/app/[locale]/email/verify/[id]/page.tsx @@ -1,12 +1,44 @@ import { Suspense } from 'react'; import EmailVerifyPage from './EmailVerifyPage'; 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 pending verification + const isPending = location.tenantEmailStatus === EmailStatus.VerificationPending; + return (
Loading...
}> - + ); diff --git a/web-app/messages/en.json b/web-app/messages/en.json index 330f352..341bc2f 100644 --- a/web-app/messages/en.json +++ b/web-app/messages/en.json @@ -455,6 +455,10 @@ "error": { "title": "Verification Failed", "unknown": "An error occurred during verification. Please try again or contact your landlord." + }, + "not-allowed": { + "title": "Action not possible", + "message": "The selected action cannot be performed or the passed information is invalid." } }, "email-unsubscribe-page": { diff --git a/web-app/messages/hr.json b/web-app/messages/hr.json index 5fd356e..6030509 100644 --- a/web-app/messages/hr.json +++ b/web-app/messages/hr.json @@ -452,6 +452,10 @@ "error": { "title": "Potvrda Nije Uspjela", "unknown": "Došlo je do greške prilikom potvrde. Molimo pokušajte ponovno ili kontaktirajte vašeg vlasnika nekretnine." + }, + "not-allowed": { + "title": "Akcija nije moguća", + "message": "Odabrana akcija nije moguća ili zadani podaci nisu ispravni." } }, "email-unsubscribe-page": {