Skip to content

Commit bda8ee7

Browse files
authored
fix(security): strengthen email invite validation logic, fix invite page UI (#1162)
* fix(security): strengthen email ivnite validation logic, fix invite page UI * ui
1 parent 104d34c commit bda8ee7

File tree

7 files changed

+360
-101
lines changed

7 files changed

+360
-101
lines changed

apps/sim/app/api/organizations/[id]/members/[memberId]/route.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@ export async function PUT(
190190
)
191191
}
192192

193+
// Prevent admins from changing other admins' roles - only owners can modify admin roles
194+
if (targetMember[0].role === 'admin' && userMember[0].role !== 'owner') {
195+
return NextResponse.json({ error: 'Only owners can change admin roles' }, { status: 403 })
196+
}
197+
193198
// Update member role
194199
const updatedMember = await db
195200
.update(member)

apps/sim/app/api/organizations/invitations/accept/route.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getSession } from '@/lib/auth'
55
import { env } from '@/lib/env'
66
import { createLogger } from '@/lib/logs/console/logger'
77
import { db } from '@/db'
8-
import { invitation, member, permissions, workspaceInvitation } from '@/db/schema'
8+
import { invitation, member, permissions, user, workspaceInvitation } from '@/db/schema'
99

1010
const logger = createLogger('OrganizationInvitationAcceptanceAPI')
1111

@@ -70,11 +70,33 @@ export async function GET(req: NextRequest) {
7070
)
7171
}
7272

73+
// Get user data to check email verification status
74+
const userData = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
75+
76+
if (userData.length === 0) {
77+
return NextResponse.redirect(
78+
new URL(
79+
'/invite/invite-error?reason=user-not-found',
80+
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
81+
)
82+
)
83+
}
84+
85+
// Check if user's email is verified
86+
if (!userData[0].emailVerified) {
87+
return NextResponse.redirect(
88+
new URL(
89+
`/invite/invite-error?reason=email-not-verified&details=${encodeURIComponent(`You must verify your email address (${userData[0].email}) before accepting invitations.`)}`,
90+
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
91+
)
92+
)
93+
}
94+
7395
// Verify the email matches the current user
7496
if (orgInvitation.email !== session.user.email) {
7597
return NextResponse.redirect(
7698
new URL(
77-
'/invite/invite-error?reason=email-mismatch',
99+
`/invite/invite-error?reason=email-mismatch&details=${encodeURIComponent(`Invitation was sent to ${orgInvitation.email}, but you're logged in as ${userData[0].email}`)}`,
78100
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
79101
)
80102
)
@@ -235,6 +257,24 @@ export async function POST(req: NextRequest) {
235257
return NextResponse.json({ error: 'Invitation already processed' }, { status: 400 })
236258
}
237259

260+
// Get user data to check email verification status
261+
const userData = await db.select().from(user).where(eq(user.id, session.user.id)).limit(1)
262+
263+
if (userData.length === 0) {
264+
return NextResponse.json({ error: 'User not found' }, { status: 404 })
265+
}
266+
267+
// Check if user's email is verified
268+
if (!userData[0].emailVerified) {
269+
return NextResponse.json(
270+
{
271+
error: 'Email not verified',
272+
message: `You must verify your email address (${userData[0].email}) before accepting invitations.`,
273+
},
274+
{ status: 403 }
275+
)
276+
}
277+
238278
if (orgInvitation.email !== session.user.email) {
239279
return NextResponse.json({ error: 'Email mismatch' }, { status: 403 })
240280
}

apps/sim/app/api/workspaces/invitations/accept/route.ts

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ export async function GET(req: NextRequest) {
1111
const token = req.nextUrl.searchParams.get('token')
1212

1313
if (!token) {
14-
// Redirect to a page explaining the error
1514
return NextResponse.redirect(
1615
new URL(
1716
'/invite/invite-error?reason=missing-token',
@@ -68,40 +67,39 @@ export async function GET(req: NextRequest) {
6867
const userEmail = session.user.email.toLowerCase()
6968
const invitationEmail = invitation.email.toLowerCase()
7069

71-
// Check if the logged-in user's email matches the invitation
72-
// We'll use exact matching as the primary check
73-
const isExactMatch = userEmail === invitationEmail
74-
75-
// For SSO or company email variants, check domain and normalized username
76-
// This handles cases like john.doe@company.com vs [email protected]
77-
const normalizeUsername = (email: string): string => {
78-
return email
79-
.split('@')[0]
80-
.replace(/[^a-zA-Z0-9]/g, '')
81-
.toLowerCase()
70+
// Get user data to check email verification status and for error messages
71+
const userData = await db
72+
.select()
73+
.from(user)
74+
.where(eq(user.id, session.user.id))
75+
.then((rows) => rows[0])
76+
77+
if (!userData) {
78+
return NextResponse.redirect(
79+
new URL(
80+
'/invite/invite-error?reason=user-not-found',
81+
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
82+
)
83+
)
8284
}
8385

84-
const isSameDomain = userEmail.split('@')[1] === invitationEmail.split('@')[1]
85-
const normalizedUserEmail = normalizeUsername(userEmail)
86-
const normalizedInvitationEmail = normalizeUsername(invitationEmail)
87-
const isSimilarUsername =
88-
normalizedUserEmail === normalizedInvitationEmail ||
89-
normalizedUserEmail.includes(normalizedInvitationEmail) ||
90-
normalizedInvitationEmail.includes(normalizedUserEmail)
86+
// Check if user's email is verified
87+
if (!userData.emailVerified) {
88+
return NextResponse.redirect(
89+
new URL(
90+
`/invite/invite-error?reason=email-not-verified&details=${encodeURIComponent(`You must verify your email address (${userData.email}) before accepting invitations.`)}`,
91+
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
92+
)
93+
)
94+
}
9195

92-
const isValidMatch = isExactMatch || (isSameDomain && isSimilarUsername)
96+
// Check if the logged-in user's email matches the invitation
97+
const isValidMatch = userEmail === invitationEmail
9398

9499
if (!isValidMatch) {
95-
// Get user info to include in the error message
96-
const userData = await db
97-
.select()
98-
.from(user)
99-
.where(eq(user.id, session.user.id))
100-
.then((rows) => rows[0])
101-
102100
return NextResponse.redirect(
103101
new URL(
104-
`/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}`)}`,
102+
`/invite/invite-error?reason=email-mismatch&details=${encodeURIComponent(`Invitation was sent to ${invitation.email}, but you're logged in as ${userData.email}`)}`,
105103
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
106104
)
107105
)

apps/sim/app/globals.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@
122122
--popover-foreground: 0 0% 98%;
123123

124124
/* Primary Colors */
125-
--primary: 0 0% 98%;
126-
--primary-foreground: 0 0% 11.2%;
125+
--primary: 0 0% 11.2%;
126+
--primary-foreground: 0 0% 98%;
127127

128128
/* Secondary Colors */
129129
--secondary: 0 0% 12.0%;

0 commit comments

Comments
 (0)