feat: invalid ref link / register protect

This commit is contained in:
DeathKaioken 2025-10-16 08:23:19 +02:00
parent f93b053569
commit bc8df1938b
3 changed files with 232 additions and 7 deletions

View File

@ -8,6 +8,7 @@ interface RegisterFormProps {
setMode: (mode: 'personal' | 'company') => void setMode: (mode: 'personal' | 'company') => void
refToken: string | null refToken: string | null
onRegistered: () => void onRegistered: () => void
referrerEmail?: string
} }
interface PersonalFormData { interface PersonalFormData {
@ -35,7 +36,8 @@ export default function RegisterForm({
mode, mode,
setMode, setMode,
refToken, refToken,
onRegistered onRegistered,
referrerEmail
}: RegisterFormProps) { }: RegisterFormProps) {
// Personal form state // Personal form state
const [personalForm, setPersonalForm] = useState<PersonalFormData>({ const [personalForm, setPersonalForm] = useState<PersonalFormData>({
@ -260,9 +262,10 @@ export default function RegisterForm({
<h2 className="text-2xl sm:text-3xl font-extrabold text-[#0F172A] mb-2"> <h2 className="text-2xl sm:text-3xl font-extrabold text-[#0F172A] mb-2">
Registrierung für Profit Planet Registrierung für Profit Planet
</h2> </h2>
{refToken && ( {/* Replace generic invite with referrer email inside the form */}
{referrerEmail && (
<p className="text-base sm:text-sm text-[#8D6B1D] font-medium"> <p className="text-base sm:text-sm text-[#8D6B1D] font-medium">
Du wurdest eingeladen! Du wurdest von <span className="font-semibold">{referrerEmail}</span> eingeladen!
</p> </p>
)} )}
</div> </div>

View File

@ -0,0 +1,67 @@
'use client'
import React from 'react'
import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'
interface InvalidRefLinkModalProps {
open: boolean
inline?: boolean
token?: string | null
onClose?: () => void
onGoHome?: () => void
}
export default function InvalidRefLinkModal({
open,
inline = true,
token,
onClose,
onGoHome
}: InvalidRefLinkModalProps) {
if (!open) return null
const Content = (
<div className="w-full max-w-md rounded-lg border border-red-200 bg-white p-6 shadow-md">
<div className="flex items-start gap-3">
<ExclamationTriangleIcon className="h-6 w-6 text-red-600 shrink-0" />
<div>
<h3 className="text-lg font-semibold text-gray-900">Invalid invitation link</h3>
<p className="mt-1 text-sm text-gray-600">
This registration link is invalid or no longer active. Please request a new link.
</p>
{token ? (
<p className="mt-2 text-xs text-gray-500">
Token: <span className="font-mono break-all">{token}</span>
</p>
) : null}
<div className="mt-4 flex items-center gap-2">
<button
onClick={onGoHome}
className="inline-flex items-center rounded-md bg-[#8D6B1D] px-3 py-2 text-sm text-white hover:bg-[#7A5E1A]"
>
Go to homepage
</button>
{onClose && (
<button
onClick={onClose}
className="inline-flex items-center rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-800 hover:bg-gray-50"
>
Close
</button>
)}
</div>
</div>
</div>
</div>
)
if (inline) {
return (
<div className="w-full flex items-center justify-center py-16">
{Content}
</div>
)
}
return Content
}

View File

@ -6,6 +6,7 @@ import useAuthStore from '../store/authStore'
import RegisterForm from './components/RegisterForm' import RegisterForm from './components/RegisterForm'
import PageLayout from '../components/PageLayout' import PageLayout from '../components/PageLayout'
import SessionDetectedModal from './components/SessionDetectedModal' import SessionDetectedModal from './components/SessionDetectedModal'
import InvalidRefLinkModal from './components/invalidRefLinkModal'
export default function RegisterPage() { export default function RegisterPage() {
const searchParams = useSearchParams() const searchParams = useSearchParams()
@ -22,6 +23,16 @@ export default function RegisterPage() {
const [showSessionModal, setShowSessionModal] = useState(false) const [showSessionModal, setShowSessionModal] = useState(false)
const [sessionCleared, setSessionCleared] = useState(false) const [sessionCleared, setSessionCleared] = useState(false)
// NEW: Referral validation state
const [isRefChecked, setIsRefChecked] = useState(false)
const [invalidRef, setInvalidRef] = useState(false)
const [refInfo, setRefInfo] = useState<{
referrerName?: string
referrerEmail?: string
isUnlimited?: boolean
usesRemaining?: number
} | null>(null)
// Redirect to login after simulated registration // Redirect to login after simulated registration
useEffect(() => { useEffect(() => {
if (registered) { if (registered) {
@ -30,10 +41,68 @@ export default function RegisterPage() {
} }
}, [registered, router]) }, [registered, router])
// Detect existing logged-in session // NEW: Validate referral token (must exist and be valid)
useEffect(() => { useEffect(() => {
if (user && !sessionCleared) setShowSessionModal(true) let cancelled = false
}, [user, sessionCleared])
const validateRef = async () => {
if (!refToken) {
console.warn('⚠️ Register: Missing ?ref token in URL')
if (!cancelled) {
setInvalidRef(true)
setIsRefChecked(true)
}
return
}
const base = process.env.NEXT_PUBLIC_API_BASE_URL || ''
const url = `${base}/api/referral/info/${encodeURIComponent(refToken)}`
console.log('🌐 Register: fetching referral info:', url)
try {
const res = await fetch(url, { method: 'GET', credentials: 'include' })
console.log('📡 Register: referral info status:', res.status)
const body = await res.json().catch(() => null)
console.log('📦 Register: referral info body:', body)
const success = !!body?.success
const isUnlimited = !!body?.isUnlimited
const usesRemaining = typeof body?.usesRemaining === 'number' ? body.usesRemaining : 0
const isActive = success && (isUnlimited || usesRemaining > 0)
if (!cancelled) {
if (isActive) {
setRefInfo({
referrerName: body?.referrerName,
referrerEmail: body?.referrerEmail,
isUnlimited,
usesRemaining
})
setInvalidRef(false)
} else {
console.warn('⛔ Register: referral not active/invalid')
setInvalidRef(true)
}
setIsRefChecked(true)
}
} catch (e) {
console.error('❌ Register: referral info fetch error:', e)
if (!cancelled) {
setInvalidRef(true)
setIsRefChecked(true)
}
}
}
validateRef()
return () => { cancelled = true }
}, [refToken])
// Detect existing logged-in session (only if ref is valid)
useEffect(() => {
if (isRefChecked && !invalidRef && user && !sessionCleared) setShowSessionModal(true)
}, [isRefChecked, invalidRef, user, sessionCleared])
const handleLogout = async () => { const handleLogout = async () => {
await logout() await logout()
@ -46,11 +115,95 @@ export default function RegisterPage() {
router.push('/dashboard') router.push('/dashboard')
} }
// NEW: Gate rendering until referral check is done
if (!isRefChecked) {
return (
<PageLayout>
<main className="w-full flex flex-col flex-1 items-center justify-center py-24">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#8D6B1D] mx-auto mb-4"></div>
<p className="text-slate-700">Überprüfe Einladungslink</p>
</div>
</main>
</PageLayout>
)
}
// NEW: Invalid referral link state — show modal instead of form with same background as register form
if (invalidRef) {
return (
<PageLayout>
<main className="w-full flex flex-col flex-1 gap-10">
<div className="relative overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
{/* Pattern */}
<svg
aria-hidden="true"
className="absolute inset-0 -z-10 h-full w-full stroke-white/10"
>
<defs>
<pattern
id="register-pattern"
x="50%"
y={-1}
width={200}
height={200}
patternUnits="userSpaceOnUse"
>
<path
d="M.5 200V.5H200"
fill="none"
stroke="rgba(255,255,255,0.05)"
/>
</pattern>
</defs>
<rect
fill="url(#register-pattern)"
width="100%"
height="100%"
strokeWidth={0}
/>
</svg>
{/* Colored blur */}
<div
aria-hidden="true"
className="absolute top-0 right-0 left-1/2 -z-10 -ml-24 transform-gpu overflow-hidden blur-3xl lg:ml-24 xl:ml-48"
>
<div
className="aspect-[801/1036] w-[50.0625rem] bg-gradient-to-tr from-[#ff80b5] to-[#9089fc] opacity-50"
style={{
clipPath:
'polygon(63.1% 29.5%, 100% 17.1%, 76.6% 3%, 48.4% 0%, 44.6% 4.7%, 54.5% 25.3%, 59.8% 49%, 55.2% 57.8%, 44.4% 57.2%, 27.8% 47.9%, 35.1% 81.5%, 0% 97.7%, 39.2% 100%, 35.2% 81.4%, 97.2% 52.8%, 63.1% 29.5%)'
}}
/>
</div>
{/* Additional background layers */}
<div className="absolute inset-0 -z-20 bg-gradient-to-b from-gray-900/95 via-gray-900/80 to-gray-900" />
<div className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(circle_at_30%_20%,rgba(255,255,255,0.1),transparent_65%)]" />
<div className="mx-auto max-w-7xl px-6 lg:px-8 relative z-10">
<div className="flex flex-col flex-1 items-center justify-center">
<InvalidRefLinkModal
inline
open
token={refToken}
onGoHome={() => router.push('/')}
onClose={() => router.push('/')}
/>
</div>
</div>
</div>
</main>
</PageLayout>
)
}
return ( return (
<PageLayout> <PageLayout>
<main className="w-full flex flex-col flex-1 gap-10"> <main className="w-full flex flex-col flex-1 gap-10">
{/* Background section wrapper */} {/* Background section wrapper */}
<div className="relative pt-16 sm:pt-20 pb-20 sm:pb-24"> <div className="relative overflow-hidden pt-16 sm:pt-20 pb-20 sm:pb-24">
{/* Pattern */} {/* Pattern */}
<svg <svg
aria-hidden="true" aria-hidden="true"
@ -123,12 +276,14 @@ export default function RegisterPage() {
</div> </div>
) : ( ) : (
<> <>
{/* Register form (only if ref valid) */}
{(!user || sessionCleared) && ( {(!user || sessionCleared) && (
<RegisterForm <RegisterForm
mode={mode} mode={mode}
setMode={setMode} setMode={setMode}
refToken={refToken} refToken={refToken}
onRegistered={() => setRegistered(true)} onRegistered={() => setRegistered(true)}
referrerEmail={refInfo?.referrerEmail}
/> />
)} )}
{registered && ( {registered && (