mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(security): strengthen email invite validation logic, fix invite page UI (#1162)
* fix(security): strengthen email ivnite validation logic, fix invite page UI * ui
This commit is contained in:
@@ -190,6 +190,11 @@ export async function PUT(
|
||||
)
|
||||
}
|
||||
|
||||
// Prevent admins from changing other admins' roles - only owners can modify admin roles
|
||||
if (targetMember[0].role === 'admin' && userMember[0].role !== 'owner') {
|
||||
return NextResponse.json({ error: 'Only owners can change admin roles' }, { status: 403 })
|
||||
}
|
||||
|
||||
// Update member role
|
||||
const updatedMember = await db
|
||||
.update(member)
|
||||
|
||||
@@ -5,7 +5,7 @@ import { getSession } from '@/lib/auth'
|
||||
import { env } from '@/lib/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { db } from '@/db'
|
||||
import { invitation, member, permissions, workspaceInvitation } from '@/db/schema'
|
||||
import { invitation, member, permissions, user, workspaceInvitation } from '@/db/schema'
|
||||
|
||||
const logger = createLogger('OrganizationInvitationAcceptanceAPI')
|
||||
|
||||
@@ -70,11 +70,33 @@ export async function GET(req: NextRequest) {
|
||||
)
|
||||
}
|
||||
|
||||
// Get user data to check email verification status
|
||||
const userData = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
|
||||
|
||||
if (userData.length === 0) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
'/invite/invite-error?reason=user-not-found',
|
||||
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user's email is verified
|
||||
if (!userData[0].emailVerified) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
`/invite/invite-error?reason=email-not-verified&details=${encodeURIComponent(`You must verify your email address (${userData[0].email}) before accepting invitations.`)}`,
|
||||
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Verify the email matches the current user
|
||||
if (orgInvitation.email !== session.user.email) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
'/invite/invite-error?reason=email-mismatch',
|
||||
`/invite/invite-error?reason=email-mismatch&details=${encodeURIComponent(`Invitation was sent to ${orgInvitation.email}, but you're logged in as ${userData[0].email}`)}`,
|
||||
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
|
||||
)
|
||||
)
|
||||
@@ -235,6 +257,24 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'Invitation already processed' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Get user data to check email verification status
|
||||
const userData = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
|
||||
|
||||
if (userData.length === 0) {
|
||||
return NextResponse.json({ error: 'User not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
// Check if user's email is verified
|
||||
if (!userData[0].emailVerified) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'Email not verified',
|
||||
message: `You must verify your email address (${userData[0].email}) before accepting invitations.`,
|
||||
},
|
||||
{ status: 403 }
|
||||
)
|
||||
}
|
||||
|
||||
if (orgInvitation.email !== session.user.email) {
|
||||
return NextResponse.json({ error: 'Email mismatch' }, { status: 403 })
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ export async function GET(req: NextRequest) {
|
||||
const token = req.nextUrl.searchParams.get('token')
|
||||
|
||||
if (!token) {
|
||||
// Redirect to a page explaining the error
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
'/invite/invite-error?reason=missing-token',
|
||||
@@ -68,40 +67,39 @@ export async function GET(req: NextRequest) {
|
||||
const userEmail = session.user.email.toLowerCase()
|
||||
const invitationEmail = invitation.email.toLowerCase()
|
||||
|
||||
// Check if the logged-in user's email matches the invitation
|
||||
// We'll use exact matching as the primary check
|
||||
const isExactMatch = userEmail === invitationEmail
|
||||
|
||||
// For SSO or company email variants, check domain and normalized username
|
||||
// This handles cases like john.doe@company.com vs john@company.com
|
||||
const normalizeUsername = (email: string): string => {
|
||||
return email
|
||||
.split('@')[0]
|
||||
.replace(/[^a-zA-Z0-9]/g, '')
|
||||
.toLowerCase()
|
||||
}
|
||||
|
||||
const isSameDomain = userEmail.split('@')[1] === invitationEmail.split('@')[1]
|
||||
const normalizedUserEmail = normalizeUsername(userEmail)
|
||||
const normalizedInvitationEmail = normalizeUsername(invitationEmail)
|
||||
const isSimilarUsername =
|
||||
normalizedUserEmail === normalizedInvitationEmail ||
|
||||
normalizedUserEmail.includes(normalizedInvitationEmail) ||
|
||||
normalizedInvitationEmail.includes(normalizedUserEmail)
|
||||
|
||||
const isValidMatch = isExactMatch || (isSameDomain && isSimilarUsername)
|
||||
|
||||
if (!isValidMatch) {
|
||||
// Get user info to include in the error message
|
||||
const userData = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.id, session.user.id))
|
||||
.then((rows) => rows[0])
|
||||
// Get user data to check email verification status and for error messages
|
||||
const userData = await db
|
||||
.select()
|
||||
.from(user)
|
||||
.where(eq(user.id, session.user.id))
|
||||
.then((rows) => rows[0])
|
||||
|
||||
if (!userData) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
`/invite/invite-error?reason=email-mismatch&details=${encodeURIComponent(`Invitation was sent to ${invitation.email}, but you're logged in as ${userData?.email || session.user.email}`)}`,
|
||||
'/invite/invite-error?reason=user-not-found',
|
||||
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Check if user's email is verified
|
||||
if (!userData.emailVerified) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
`/invite/invite-error?reason=email-not-verified&details=${encodeURIComponent(`You must verify your email address (${userData.email}) before accepting invitations.`)}`,
|
||||
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Check if the logged-in user's email matches the invitation
|
||||
const isValidMatch = userEmail === invitationEmail
|
||||
|
||||
if (!isValidMatch) {
|
||||
return NextResponse.redirect(
|
||||
new URL(
|
||||
`/invite/invite-error?reason=email-mismatch&details=${encodeURIComponent(`Invitation was sent to ${invitation.email}, but you're logged in as ${userData.email}`)}`,
|
||||
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
|
||||
)
|
||||
)
|
||||
|
||||
@@ -122,8 +122,8 @@
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
/* Primary Colors */
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 11.2%;
|
||||
--primary: 0 0% 11.2%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
/* Secondary Colors */
|
||||
--secondary: 0 0% 12.0%;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { BotIcon, CheckCircle } from 'lucide-react'
|
||||
import { AlertCircle, CheckCircle2, Mail, UserPlus, Users2 } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { LoadingAgent } from '@/components/ui/loading-agent'
|
||||
import { client, useSession } from '@/lib/auth-client'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
|
||||
export default function Invite() {
|
||||
const router = useRouter()
|
||||
@@ -14,6 +15,7 @@ export default function Invite() {
|
||||
const inviteId = params.id as string
|
||||
const searchParams = useSearchParams()
|
||||
const { data: session, isPending } = useSession()
|
||||
const brandConfig = useBrandConfig()
|
||||
const [invitationDetails, setInvitationDetails] = useState<any>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -174,28 +176,46 @@ export default function Invite() {
|
||||
const callbackUrl = encodeURIComponent(getCallbackUrl())
|
||||
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-muted/40 p-4'>
|
||||
<Card className='w-full max-w-md p-6'>
|
||||
<CardHeader className='px-0 pt-0 text-center'>
|
||||
<CardTitle>You've been invited to join a workspace</CardTitle>
|
||||
<CardDescription>
|
||||
{isNewUser
|
||||
? 'Create an account to join this workspace on Sim'
|
||||
: 'Sign in to your account to accept this invitation'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardFooter className='flex flex-col space-y-2 px-0'>
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-white px-4 dark:bg-black'>
|
||||
<div className='mb-8'>
|
||||
<Image
|
||||
src={brandConfig.logoUrl || '/logo/b&w/medium.png'}
|
||||
alt='Sim Logo'
|
||||
width={120}
|
||||
height={67}
|
||||
className='dark:invert'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex w-full max-w-md flex-col items-center text-center'>
|
||||
<div className='mb-6 rounded-full bg-blue-50 p-3 dark:bg-blue-950/20'>
|
||||
<UserPlus className='h-8 w-8 text-blue-500 dark:text-blue-400' />
|
||||
</div>
|
||||
|
||||
<h1 className='mb-2 font-semibold text-black text-xl dark:text-white'>
|
||||
You've been invited!
|
||||
</h1>
|
||||
|
||||
<p className='mb-6 text-gray-600 text-sm leading-relaxed dark:text-gray-300'>
|
||||
{isNewUser
|
||||
? 'Create an account to join this workspace on Sim'
|
||||
: 'Sign in to your account to accept this invitation'}
|
||||
</p>
|
||||
|
||||
<div className='flex w-full flex-col gap-3'>
|
||||
{isNewUser ? (
|
||||
<>
|
||||
<Button
|
||||
className='w-full'
|
||||
style={{ backgroundColor: 'var(--brand-primary-hex)', color: 'white' }}
|
||||
onClick={() => router.push(`/signup?callbackUrl=${callbackUrl}&invite_flow=true`)}
|
||||
>
|
||||
Create an account
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full'
|
||||
className='w-full border-brand-primary text-brand-primary hover:bg-brand-primary hover:text-white'
|
||||
onClick={() => router.push(`/login?callbackUrl=${callbackUrl}&invite_flow=true`)}
|
||||
>
|
||||
I already have an account
|
||||
@@ -205,13 +225,14 @@ export default function Invite() {
|
||||
<>
|
||||
<Button
|
||||
className='w-full'
|
||||
style={{ backgroundColor: 'var(--brand-primary-hex)', color: 'white' }}
|
||||
onClick={() => router.push(`/login?callbackUrl=${callbackUrl}&invite_flow=true`)}
|
||||
>
|
||||
Sign in
|
||||
</Button>
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full'
|
||||
className='w-full border-brand-primary text-brand-primary hover:bg-brand-primary hover:text-white'
|
||||
onClick={() =>
|
||||
router.push(`/signup?callbackUrl=${callbackUrl}&invite_flow=true&new=true`)
|
||||
}
|
||||
@@ -220,8 +241,23 @@ export default function Invite() {
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Button
|
||||
className='w-full'
|
||||
style={{ backgroundColor: 'var(--brand-primary-hex)', color: 'white' }}
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
Return to Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className='mt-8 text-center text-gray-500 text-xs'>
|
||||
Need help?{' '}
|
||||
<a href='mailto:help@sim.ai' className='text-blue-400 hover:text-blue-300'>
|
||||
Contact support
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -229,9 +265,26 @@ export default function Invite() {
|
||||
// Show loading state
|
||||
if (isLoading || isPending) {
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-muted/40 p-4'>
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-white px-4 dark:bg-black'>
|
||||
<div className='mb-8'>
|
||||
<Image
|
||||
src={brandConfig.logoUrl || '/logo/b&w/medium.png'}
|
||||
alt='Sim Logo'
|
||||
width={120}
|
||||
height={67}
|
||||
className='dark:invert'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<LoadingAgent size='lg' />
|
||||
<p className='mt-4 text-muted-foreground text-sm'>Loading invitation...</p>
|
||||
<p className='mt-4 text-gray-400 text-sm'>Loading invitation...</p>
|
||||
|
||||
<footer className='mt-8 text-center text-gray-500 text-xs'>
|
||||
Need help?{' '}
|
||||
<a href='mailto:help@sim.ai' className='text-blue-400 hover:text-blue-300'>
|
||||
Contact support
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -239,14 +292,41 @@ export default function Invite() {
|
||||
// Show error state
|
||||
if (error) {
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-muted/40 p-4'>
|
||||
<Card className='max-w-md space-y-2 p-6 text-center'>
|
||||
<div className='flex justify-center'>
|
||||
<BotIcon className='h-16 w-16 text-muted-foreground' />
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-white px-4 dark:bg-black'>
|
||||
<div className='mb-8'>
|
||||
<Image
|
||||
src={brandConfig.logoUrl || '/logo/b&w/medium.png'}
|
||||
alt='Sim Logo'
|
||||
width={120}
|
||||
height={67}
|
||||
className='dark:invert'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className='flex w-full max-w-md flex-col items-center text-center'>
|
||||
<div className='mb-6 rounded-full bg-red-50 p-3 dark:bg-red-950/20'>
|
||||
<AlertCircle className='h-8 w-8 text-red-500 dark:text-red-400' />
|
||||
</div>
|
||||
<h3 className='font-semibold text-lg'>Invitation Error</h3>
|
||||
<p className='text-muted-foreground'>{error}</p>
|
||||
</Card>
|
||||
<h1 className='mb-2 font-semibold text-black text-xl dark:text-white'>
|
||||
Invitation Error
|
||||
</h1>
|
||||
<p className='mb-6 text-gray-600 text-sm leading-relaxed dark:text-gray-300'>{error}</p>
|
||||
|
||||
<Button
|
||||
className='w-full'
|
||||
style={{ backgroundColor: 'var(--brand-primary-hex)', color: 'white' }}
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
Return to Home
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<footer className='mt-8 text-center text-gray-500 text-xs'>
|
||||
Need help?{' '}
|
||||
<a href='mailto:help@sim.ai' className='text-blue-400 hover:text-blue-300'>
|
||||
Contact support
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -254,41 +334,113 @@ export default function Invite() {
|
||||
// Show success state
|
||||
if (accepted) {
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-muted/40 p-4'>
|
||||
<Card className='max-w-md space-y-2 p-6 text-center'>
|
||||
<div className='flex justify-center'>
|
||||
<CheckCircle className='h-16 w-16 text-green-500' />
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-white px-4 dark:bg-black'>
|
||||
<div className='mb-8'>
|
||||
<Image
|
||||
src={brandConfig.logoUrl || '/logo/b&w/medium.png'}
|
||||
alt='Sim Logo'
|
||||
width={120}
|
||||
height={67}
|
||||
className='dark:invert'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<div className='flex w-full max-w-md flex-col items-center text-center'>
|
||||
<div className='mb-6 rounded-full bg-green-50 p-3 dark:bg-green-950/20'>
|
||||
<CheckCircle2 className='h-8 w-8 text-green-500 dark:text-green-400' />
|
||||
</div>
|
||||
<h3 className='font-semibold text-lg'>Invitation Accepted</h3>
|
||||
<p className='text-muted-foreground'>
|
||||
<h1 className='mb-2 font-semibold text-black text-xl dark:text-white'>Welcome!</h1>
|
||||
<p className='mb-6 text-gray-600 text-sm leading-relaxed dark:text-gray-300'>
|
||||
You have successfully joined {invitationDetails?.name || 'the workspace'}. Redirecting
|
||||
to your workspace...
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
<Button
|
||||
className='w-full'
|
||||
style={{ backgroundColor: 'var(--brand-primary-hex)', color: 'white' }}
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
Return to Home
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<footer className='mt-8 text-center text-gray-500 text-xs'>
|
||||
Need help?{' '}
|
||||
<a href='mailto:help@sim.ai' className='text-blue-400 hover:text-blue-300'>
|
||||
Contact support
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Show invitation details
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-muted/40 p-4'>
|
||||
<Card className='w-full max-w-md'>
|
||||
<CardHeader className='text-center'>
|
||||
<CardTitle className='mb-1'>Workspace Invitation</CardTitle>
|
||||
<CardDescription className='text-md'>
|
||||
You've been invited to join{' '}
|
||||
<span className='font-medium'>{invitationDetails?.name || 'a workspace'}</span>
|
||||
</CardDescription>
|
||||
<p className='mt-2 text-md text-muted-foreground'>
|
||||
Click the accept below to join the workspace.
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardFooter className='flex justify-center'>
|
||||
<Button onClick={handleAcceptInvitation} disabled={isAccepting} className='w-full'>
|
||||
<span className='ml-2'>{isAccepting ? '' : ''}Accept Invitation</span>
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-white px-4 dark:bg-black'>
|
||||
<div className='mb-8'>
|
||||
<Image
|
||||
src='/logo/b&w/medium.png'
|
||||
alt='Sim Logo'
|
||||
width={120}
|
||||
height={67}
|
||||
className='dark:invert'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex w-full max-w-md flex-col items-center text-center'>
|
||||
<div className='mb-6 rounded-full bg-blue-50 p-3 dark:bg-blue-950/20'>
|
||||
{invitationType === 'organization' ? (
|
||||
<Users2 className='h-8 w-8 text-blue-500 dark:text-blue-400' />
|
||||
) : (
|
||||
<Mail className='h-8 w-8 text-blue-500 dark:text-blue-400' />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h1 className='mb-2 font-semibold text-black text-xl dark:text-white'>
|
||||
{invitationType === 'organization' ? 'Organization Invitation' : 'Workspace Invitation'}
|
||||
</h1>
|
||||
|
||||
<p className='mb-6 text-gray-600 text-sm leading-relaxed dark:text-gray-300'>
|
||||
You've been invited to join{' '}
|
||||
<span className='font-medium text-black dark:text-white'>
|
||||
{invitationDetails?.name || `a ${invitationType}`}
|
||||
</span>
|
||||
. Click accept below to join.
|
||||
</p>
|
||||
|
||||
<div className='flex w-full flex-col gap-3'>
|
||||
<Button
|
||||
onClick={handleAcceptInvitation}
|
||||
disabled={isAccepting}
|
||||
className='w-full'
|
||||
style={{ backgroundColor: 'var(--brand-primary-hex)', color: 'white' }}
|
||||
>
|
||||
{isAccepting ? (
|
||||
<>
|
||||
<LoadingAgent size='sm' />
|
||||
Accepting...
|
||||
</>
|
||||
) : (
|
||||
'Accept Invitation'
|
||||
)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
<Button
|
||||
variant='ghost'
|
||||
className='w-full text-gray-600 hover:bg-gray-200 hover:text-black dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white'
|
||||
onClick={() => router.push('/')}
|
||||
>
|
||||
Return to Home
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className='mt-8 text-center text-gray-500 text-xs'>
|
||||
Need help?{' '}
|
||||
<a href='mailto:help@sim.ai' className='text-blue-400 hover:text-blue-300'>
|
||||
Contact support
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import { Mail, RotateCcw, ShieldX } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { useBrandConfig } from '@/lib/branding/branding'
|
||||
|
||||
function getErrorMessage(reason: string, details?: string): string {
|
||||
switch (reason) {
|
||||
@@ -22,6 +24,18 @@ function getErrorMessage(reason: string, details?: string): string {
|
||||
: 'This invitation was sent to a different email address than the one you are logged in with.'
|
||||
case 'workspace-not-found':
|
||||
return 'The workspace associated with this invitation could not be found.'
|
||||
case 'user-not-found':
|
||||
return 'Your user account could not be found. Please try logging out and logging back in.'
|
||||
case 'email-not-verified':
|
||||
return details
|
||||
? details
|
||||
: 'You must verify your email address before accepting invitations. Please check your email for a verification link.'
|
||||
case 'already-member':
|
||||
return 'You are already a member of this organization or workspace.'
|
||||
case 'invalid-invitation':
|
||||
return 'This invitation is invalid or no longer exists.'
|
||||
case 'missing-invitation-id':
|
||||
return 'The invitation link is missing required information. Please use the original invitation link.'
|
||||
case 'server-error':
|
||||
return 'An unexpected error occurred while processing your invitation. Please try again later.'
|
||||
default:
|
||||
@@ -34,6 +48,7 @@ export default function InviteError() {
|
||||
const reason = searchParams?.get('reason') || 'unknown'
|
||||
const details = searchParams?.get('details')
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
const brandConfig = useBrandConfig()
|
||||
|
||||
useEffect(() => {
|
||||
// Only set the error message on the client side
|
||||
@@ -43,31 +58,79 @@ export default function InviteError() {
|
||||
// Provide a fallback message for SSR
|
||||
const displayMessage = errorMessage || 'Loading error details...'
|
||||
|
||||
const isEmailVerificationError = reason === 'email-not-verified'
|
||||
|
||||
const isExpiredError = reason === 'expired'
|
||||
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col items-center justify-center'>
|
||||
<div className='mx-auto max-w-md rounded-lg border bg-card px-6 py-12'>
|
||||
<div className='flex flex-col items-center text-center'>
|
||||
<AlertTriangle className='mb-4 h-12 w-12 text-amber-500' />
|
||||
<div className='flex min-h-screen flex-col items-center justify-center bg-white px-4 dark:bg-black'>
|
||||
{/* Logo */}
|
||||
<div className='mb-8'>
|
||||
<Image
|
||||
src={brandConfig.logoUrl || '/logo/b&w/medium.png'}
|
||||
alt='Sim Logo'
|
||||
width={120}
|
||||
height={67}
|
||||
className='dark:invert'
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h1 className='mb-2 font-bold text-2xl tracking-tight'>Invitation Error</h1>
|
||||
<div className='flex w-full max-w-md flex-col items-center text-center'>
|
||||
<div className='mb-6 rounded-full bg-red-50 p-3 dark:bg-red-950/20'>
|
||||
<ShieldX className='h-8 w-8 text-red-500 dark:text-red-400' />
|
||||
</div>
|
||||
|
||||
<p className='mb-6 text-muted-foreground'>{displayMessage}</p>
|
||||
<h1 className='mb-2 font-semibold text-black text-xl dark:text-white'>Invitation Error</h1>
|
||||
|
||||
<div className='flex w-full flex-col gap-4'>
|
||||
<Link href='/workspace' passHref>
|
||||
<Button variant='default' className='w-full'>
|
||||
Go to Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
<p className='mb-6 text-gray-600 text-sm leading-relaxed dark:text-gray-300'>
|
||||
{displayMessage}
|
||||
</p>
|
||||
|
||||
<Link href='/' passHref>
|
||||
<Button variant='outline' className='w-full'>
|
||||
Return to Home
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div className='flex w-full flex-col gap-3'>
|
||||
{isEmailVerificationError && (
|
||||
<Button
|
||||
variant='default'
|
||||
className='w-full'
|
||||
style={{ backgroundColor: 'var(--brand-primary-hex)', color: 'white' }}
|
||||
asChild
|
||||
>
|
||||
<Link href='/verify'>
|
||||
<Mail className='mr-2 h-4 w-4' />
|
||||
Verify Email
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{isExpiredError && (
|
||||
<Button
|
||||
variant='outline'
|
||||
className='w-full border-brand-primary text-brand-primary hover:bg-brand-primary hover:text-white'
|
||||
asChild
|
||||
>
|
||||
<Link href='/'>
|
||||
<RotateCcw className='mr-2 h-4 w-4' />
|
||||
Request New Invitation
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className='w-full'
|
||||
style={{ backgroundColor: 'var(--brand-primary-hex)', color: 'white' }}
|
||||
asChild
|
||||
>
|
||||
<Link href='/'>Return to Home</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className='mt-8 text-center text-gray-500 text-xs'>
|
||||
Need help?{' '}
|
||||
<a href='mailto:help@sim.ai' className='text-blue-400 hover:text-blue-300'>
|
||||
Contact support
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ export const buildTimeCSPDirectives: CSPDirectives = {
|
||||
'style-src': ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
|
||||
|
||||
'img-src': [
|
||||
'https://agentics.epiqglobal.com',
|
||||
"'self'",
|
||||
'data:',
|
||||
'blob:',
|
||||
|
||||
Reference in New Issue
Block a user