572052285c
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>
147 lines
6.3 KiB
TypeScript
147 lines
6.3 KiB
TypeScript
'use client'
|
||
|
||
import { Suspense, useEffect, useState, useCallback } from 'react'
|
||
import { useSearchParams, useRouter } from 'next/navigation'
|
||
import { motion } from 'framer-motion'
|
||
|
||
function VerifyContent() {
|
||
const searchParams = useSearchParams()
|
||
const router = useRouter()
|
||
const token = searchParams.get('token')
|
||
|
||
const [status, setStatus] = useState<'ready' | 'verifying' | 'success' | 'error'>('ready')
|
||
const [errorMsg, setErrorMsg] = useState('')
|
||
|
||
useEffect(() => {
|
||
if (!token) {
|
||
setStatus('error')
|
||
setErrorMsg('No access token provided.')
|
||
return
|
||
}
|
||
|
||
// If the investor already has a valid session, skip the button entirely
|
||
fetch('/api/auth/me').then(res => {
|
||
if (res.ok) router.push('/')
|
||
})
|
||
}, [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 (
|
||
<motion.div
|
||
initial={{ opacity: 0, scale: 0.95 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
transition={{ duration: 0.4 }}
|
||
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">
|
||
{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' && (
|
||
<>
|
||
<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...</p>
|
||
</>
|
||
)}
|
||
|
||
{status === 'success' && (
|
||
<>
|
||
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-green-500/10 flex items-center justify-center">
|
||
<svg className="w-8 h-8 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||
</svg>
|
||
</div>
|
||
<p className="text-white/80 font-medium">Access verified!</p>
|
||
<p className="text-white/40 text-sm mt-2">Redirecting to pitch deck...</p>
|
||
</>
|
||
)}
|
||
|
||
{status === 'error' && (
|
||
<>
|
||
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-red-500/10 flex items-center justify-center">
|
||
<svg className="w-8 h-8 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||
</svg>
|
||
</div>
|
||
<p className="text-white/80 font-medium mb-2">Access Denied</p>
|
||
<p className="text-white/50 text-sm">{errorMsg}</p>
|
||
<a
|
||
href="/auth"
|
||
className="inline-block mt-6 text-indigo-400 text-sm hover:text-indigo-300 transition-colors"
|
||
>
|
||
Back to login
|
||
</a>
|
||
</>
|
||
)}
|
||
</div>
|
||
</motion.div>
|
||
)
|
||
}
|
||
|
||
export default function VerifyPage() {
|
||
return (
|
||
<div className="h-screen flex items-center justify-center bg-[#0a0a1a] relative overflow-hidden">
|
||
<div className="absolute inset-0 bg-gradient-to-br from-indigo-950/30 via-transparent to-purple-950/20" />
|
||
<Suspense
|
||
fallback={
|
||
<div className="relative z-10 text-center">
|
||
<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">Loading...</p>
|
||
</div>
|
||
}
|
||
>
|
||
<VerifyContent />
|
||
</Suspense>
|
||
|
||
<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">
|
||
<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>
|
||
<p className="text-[10px] text-white/15 text-center mt-1">
|
||
Verantwortlich: Benjamin Bönisch & Sharang Parnerkar · Kontakt: info@breakpilot.com
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|