Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
44 changes: 42 additions & 2 deletions apps/sim/app/api/organizations/invitations/accept/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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'
)
)
Expand Down Expand Up @@ -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 })
}
Expand Down
54 changes: 26 additions & 28 deletions apps/sim/app/api/workspaces/invitations/accept/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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()
// 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=user-not-found',
env.NEXT_PUBLIC_APP_URL || 'https://sim.ai'
)
)
}

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)
// 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'
)
)
}

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

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])

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=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'
)
)
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
Expand Down
Loading