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
|
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>
|
||||||
|
|||||||
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 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 && (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user