fix: require button click to consume magic link token
Build pitch-deck / build-push-deploy (push) Successful in 1m54s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 47s
CI / test-python-voice (push) Successful in 37s
CI / test-bqas (push) Successful in 37s
Build pitch-deck / build-push-deploy (push) Successful in 1m54s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 47s
CI / test-python-voice (push) Successful in 37s
CI / test-bqas (push) Successful in 37s
Email security gateways follow GET redirects automatically and were consuming the token before the investor clicked through. The verify page now shows an 'Access Pitch Deck' button; the token is only consumed on explicit click, which scanners cannot trigger. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Suspense, useEffect, useState } from 'react'
|
import { Suspense, useEffect, useState, useCallback } from 'react'
|
||||||
import { useSearchParams, useRouter } from 'next/navigation'
|
import { useSearchParams, useRouter } from 'next/navigation'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ function VerifyContent() {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const token = searchParams.get('token')
|
const token = searchParams.get('token')
|
||||||
|
|
||||||
const [status, setStatus] = useState<'verifying' | 'success' | 'error'>('verifying')
|
const [status, setStatus] = useState<'ready' | 'verifying' | 'success' | 'error'>('ready')
|
||||||
const [errorMsg, setErrorMsg] = useState('')
|
const [errorMsg, setErrorMsg] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -19,38 +19,37 @@ function VerifyContent() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
async function verify() {
|
// If the investor already has a valid session, skip the button entirely
|
||||||
try {
|
fetch('/api/auth/me').then(res => {
|
||||||
// If the investor already has a valid session, skip token verification
|
if (res.ok) router.push('/')
|
||||||
const sessionCheck = await fetch('/api/auth/me')
|
})
|
||||||
if (sessionCheck.ok) {
|
|
||||||
router.push('/')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch('/api/auth/verify', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ token }),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
setStatus('success')
|
|
||||||
setTimeout(() => router.push('/'), 1000)
|
|
||||||
} else {
|
|
||||||
const data = await res.json()
|
|
||||||
setStatus('error')
|
|
||||||
setErrorMsg(data.error || 'Verification failed.')
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
setStatus('error')
|
|
||||||
setErrorMsg('Network error. Please try again.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
verify()
|
|
||||||
}, [token, router])
|
}, [token, router])
|
||||||
|
|
||||||
|
const handleAccess = useCallback(async () => {
|
||||||
|
if (!token || status === 'verifying') return
|
||||||
|
setStatus('verifying')
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/auth/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
setStatus('success')
|
||||||
|
setTimeout(() => router.push('/'), 1000)
|
||||||
|
} else {
|
||||||
|
const data = await res.json()
|
||||||
|
setStatus('error')
|
||||||
|
setErrorMsg(data.error || 'Verification failed.')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setStatus('error')
|
||||||
|
setErrorMsg('Network error. Please try again.')
|
||||||
|
}
|
||||||
|
}, [token, status, router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, scale: 0.95 }}
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
@@ -59,10 +58,28 @@ function VerifyContent() {
|
|||||||
className="relative z-10 text-center max-w-md mx-auto px-6"
|
className="relative z-10 text-center max-w-md mx-auto px-6"
|
||||||
>
|
>
|
||||||
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 backdrop-blur-sm">
|
<div className="bg-white/[0.03] border border-white/[0.06] rounded-2xl p-8 backdrop-blur-sm">
|
||||||
|
{status === 'ready' && (
|
||||||
|
<>
|
||||||
|
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-indigo-500/10 flex items-center justify-center">
|
||||||
|
<svg className="w-8 h-8 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.069A1 1 0 0121 8.87v6.26a1 1 0 01-1.447.894L15 14M3 8a2 2 0 012-2h8a2 2 0 012 2v8a2 2 0 01-2 2H5a2 2 0 01-2-2V8z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p className="text-white/80 font-medium mb-1">Your pitch deck is ready</p>
|
||||||
|
<p className="text-white/40 text-sm mb-6">Click below to access it.</p>
|
||||||
|
<button
|
||||||
|
onClick={handleAccess}
|
||||||
|
className="w-full bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white font-medium py-3 px-6 rounded-xl transition-all shadow-lg shadow-indigo-500/20"
|
||||||
|
>
|
||||||
|
Access Pitch Deck
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{status === 'verifying' && (
|
{status === 'verifying' && (
|
||||||
<>
|
<>
|
||||||
<div className="w-12 h-12 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
<div className="w-12 h-12 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" />
|
||||||
<p className="text-white/60">Verifying your access link...</p>
|
<p className="text-white/60">Verifying your access...</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -115,11 +132,10 @@ export default function VerifyPage() {
|
|||||||
<VerifyContent />
|
<VerifyContent />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|
||||||
{/* Privacy Notice Footer */}
|
|
||||||
<div className="absolute bottom-0 left-0 right-0 z-10 px-8 py-4 border-t border-white/5">
|
<div className="absolute bottom-0 left-0 right-0 z-10 px-8 py-4 border-t border-white/5">
|
||||||
<div className="max-w-2xl mx-auto">
|
<div className="max-w-2xl mx-auto">
|
||||||
<p className="text-[10px] text-white/20 leading-relaxed text-center">
|
<p className="text-[10px] text-white/20 leading-relaxed text-center">
|
||||||
<strong className="text-white/25">Datenschutzhinweis (Art. 13 DSGVO):</strong> Beim Zugriff werden technische Zugriffsdaten (IP-Adresse, Zeitpunkt, Browser) sowie – soweit eingeladen – personenbezogene Kontaktdaten (E-Mail, Name, Unternehmen) verarbeitet. Zweck: Zugangsverwaltung und Missbrauchsprävention. Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse). Speicherdauer: max. 30 Tage nach letztem Zugriff; nicht aktivierte Zugänge nach 90 Tagen. Danach automatische Anonymisierung. Ihre Rechte gem. Art. 15–21 DSGVO (Auskunft, Berichtigung, Löschung, Einschränkung, Datenübertragbarkeit, Widerspruch): Anfragen an pitch@breakpilot.ai. Beschwerderecht bei der Aufsichtsbehörde: LfDI Baden-Württemberg (www.baden-wuerttemberg.datenschutz.de).</p>
|
<strong className="text-white/25">Datenschutzhinweis (Art. 13 DSGVO):</strong> Beim Zugriff werden technische Zugriffsdaten (IP-Adresse, Zeitpunkt, Browser) sowie – soweit eingeladen – personenbezogene Kontaktdaten (E-Mail, Name, Unternehmen) verarbeitet. Zweck: Zugangsverwaltung und Missbrauchsprävention. Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse). Speicherdauer: max. 30 Tage nach letztem Zugriff; nicht aktivierte Zugänge nach 90 Tagen. Danach automatische Anonymisierung. Ihre Rechte gem. Art. 15–21 DSGVO (Auskunft, Berichtigung, Löschung, Einschränkung, Datenübertragbarkeit, Widerspruch): Anfragen an pitch@breakpilot.ai. Beschwerderecht bei der Aufsichtsbehörde: LfDI Baden-Württemberg (www.baden-wuerttemberg.datenschutz.de).</p>
|
||||||
<p className="text-[10px] text-white/15 text-center mt-1">
|
<p className="text-[10px] text-white/15 text-center mt-1">
|
||||||
Verantwortlich: Benjamin Bönisch & Sharang Parnerkar · Kontakt: info@breakpilot.com
|
Verantwortlich: Benjamin Bönisch & Sharang Parnerkar · Kontakt: info@breakpilot.com
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user