feat: invalid ref link / register protect
This commit is contained in:
parent
f93b053569
commit
bc8df1938b
@ -8,6 +8,7 @@ interface RegisterFormProps {
|
||||
setMode: (mode: 'personal' | 'company') => void
|
||||
refToken: string | null
|
||||
onRegistered: () => void
|
||||
referrerEmail?: string
|
||||
}
|
||||
|
||||
interface PersonalFormData {
|
||||
@ -35,7 +36,8 @@ export default function RegisterForm({
|
||||
mode,
|
||||
setMode,
|
||||
refToken,
|
||||
onRegistered
|
||||
onRegistered,
|
||||
referrerEmail
|
||||
}: RegisterFormProps) {
|
||||
// Personal form state
|
||||
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">
|
||||
Registrierung für Profit Planet
|
||||
</h2>
|
||||
{refToken && (
|
||||
{/* Replace generic invite with referrer email inside the form */}
|
||||
{referrerEmail && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
67
src/app/register/components/invalidRefLinkModal.tsx
Normal file
67
src/app/register/components/invalidRefLinkModal.tsx
Normal 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
|
||||
}
|
||||
@ -6,6 +6,7 @@ import useAuthStore from '../store/authStore'
|
||||
import RegisterForm from './components/RegisterForm'
|
||||
import PageLayout from '../components/PageLayout'
|
||||
import SessionDetectedModal from './components/SessionDetectedModal'
|
||||
import InvalidRefLinkModal from './components/invalidRefLinkModal'
|
||||
|
||||
export default function RegisterPage() {
|
||||
const searchParams = useSearchParams()
|
||||
@ -22,6 +23,16 @@ export default function RegisterPage() {
|
||||
const [showSessionModal, setShowSessionModal] = 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
|
||||
useEffect(() => {
|
||||
if (registered) {
|
||||
@ -30,10 +41,68 @@ export default function RegisterPage() {
|
||||
}
|
||||
}, [registered, router])
|
||||
|
||||
// Detect existing logged-in session
|
||||
// NEW: Validate referral token (must exist and be valid)
|
||||
useEffect(() => {
|
||||
if (user && !sessionCleared) setShowSessionModal(true)
|
||||
}, [user, sessionCleared])
|
||||
let cancelled = false
|
||||
|
||||
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 () => {
|
||||
await logout()
|
||||
@ -46,11 +115,95 @@ export default function RegisterPage() {
|
||||
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 (
|
||||
<PageLayout>
|
||||
<main className="w-full flex flex-col flex-1 gap-10">
|
||||
{/* 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 */}
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
@ -123,12 +276,14 @@ export default function RegisterPage() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Register form (only if ref valid) */}
|
||||
{(!user || sessionCleared) && (
|
||||
<RegisterForm
|
||||
mode={mode}
|
||||
setMode={setMode}
|
||||
refToken={refToken}
|
||||
onRegistered={() => setRegistered(true)}
|
||||
referrerEmail={refInfo?.referrerEmail}
|
||||
/>
|
||||
)}
|
||||
{registered && (
|
||||
|
||||
Loading…
Reference in New Issue
Block a user