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
178 changes: 178 additions & 0 deletions apps/sim/app/api/workspaces/[id]/permissions/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { db } from '@/db'
import { permissions, type permissionTypeEnum, user, workspaceMember } from '@/db/schema'

// Extract the enum type from Drizzle schema
type PermissionType = (typeof permissionTypeEnum.enumValues)[number]

interface UpdatePermissionsRequest {
updates: Array<{
userId: string
permissions: PermissionType // Single permission type instead of object with booleans
}>
}

// Helper function to fetch users with permissions for a workspace
async function getUsersWithPermissions(workspaceId: string) {
const usersWithPermissions = await db
.select({
userId: user.id,
email: user.email,
name: user.name,
image: user.image,
permissionType: permissions.permissionType,
})
.from(permissions)
.innerJoin(user, eq(permissions.userId, user.id))
.where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId)))
.orderBy(user.email)

// Since each user has only one permission, we can use the results directly
return usersWithPermissions.map((row) => ({
userId: row.userId,
email: row.email,
name: row.name,
image: row.image,
permissionType: row.permissionType,
}))
}

/**
* GET /api/workspaces/[id]/permissions
*
* Retrieves all users who have permissions for the specified workspace.
* Returns user details along with their specific permissions.
*
* @param workspaceId - The workspace ID from the URL parameters
* @returns Array of users with their permissions for the workspace
*/
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id: workspaceId } = await params
const session = await getSession()

if (!session?.user?.id) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}

// Verify the current user has access to this workspace
const userMembership = await db
.select()
.from(workspaceMember)
.where(
and(
eq(workspaceMember.workspaceId, workspaceId),
eq(workspaceMember.userId, session.user.id)
)
)
.limit(1)

if (userMembership.length === 0) {
return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 })
}

const result = await getUsersWithPermissions(workspaceId)

return NextResponse.json({
users: result,
total: result.length,
})
} catch (error) {
console.error('Error fetching workspace permissions:', error)
return NextResponse.json({ error: 'Failed to fetch workspace permissions' }, { status: 500 })
}
}
Comment on lines +51 to +86
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: GET endpoint allows any workspace member to view all permissions - should be restricted to admins only for consistency with PATCH


/**
* PATCH /api/workspaces/[id]/permissions
*
* Updates permissions for existing workspace members.
* Only admin users can update permissions.
*
* @param workspaceId - The workspace ID from the URL parameters
* @param updates - Array of permission updates for users
* @returns Success message or error
*/
export async function PATCH(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try {
const { id: workspaceId } = await params
const session = await getSession()

if (!session?.user?.id) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}

// Verify the current user has admin access to this workspace
const userPermissions = await db
.select()
.from(permissions)
.where(
and(
eq(permissions.userId, session.user.id),
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId),
eq(permissions.permissionType, 'admin')
)
)
.limit(1)

if (userPermissions.length === 0) {
return NextResponse.json(
{ error: 'Admin access required to update permissions' },
{ status: 403 }
)
}

// Parse and validate request body
const body: UpdatePermissionsRequest = await request.json()

// Prevent users from modifying their own admin permissions
const selfUpdate = body.updates.find((update) => update.userId === session.user.id)
if (selfUpdate && selfUpdate.permissions !== 'admin') {
return NextResponse.json(
{ error: 'Cannot remove your own admin permissions' },
{ status: 400 }
)
}

// Process updates in a transaction
await db.transaction(async (tx) => {
for (const update of body.updates) {
// Delete existing permissions for this user and workspace
await tx
.delete(permissions)
.where(
and(
eq(permissions.userId, update.userId),
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId)
)
)

// Insert the single new permission
await tx.insert(permissions).values({
id: crypto.randomUUID(),
userId: update.userId,
entityType: 'workspace' as const,
entityId: workspaceId,
permissionType: update.permissions,
createdAt: new Date(),
updatedAt: new Date(),
})
}
})

const updatedUsers = await getUsersWithPermissions(workspaceId)

return NextResponse.json({
message: 'Permissions updated successfully',
users: updatedUsers,
total: updatedUsers.length,
})
} catch (error) {
console.error('Error updating workspace permissions:', error)
return NextResponse.json({ error: 'Failed to update workspace permissions' }, { status: 500 })
}
}
63 changes: 23 additions & 40 deletions apps/sim/app/api/workspaces/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
import { db } from '@/db'
import { workspace, workspaceMember } from '@/db/schema'
import { permissions, workspace } from '@/db/schema'

export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params
Expand All @@ -14,16 +15,9 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{

const workspaceId = id

// Check if user is a member of this workspace
const membership = await db
.select()
.from(workspaceMember)
.where(
and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, session.user.id))
)
.then((rows) => rows[0])

if (!membership) {
// Check if user has read access to this workspace
const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
if (userPermission !== 'read') {
return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 })
Comment on lines +20 to 21
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logic: Check should be 'if (!userPermission)' since getUserEntityPermissions returns null for no access. Current check allows write/admin access but denies read.

}

Expand All @@ -41,7 +35,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{
return NextResponse.json({
workspace: {
...workspaceDetails,
role: membership.role,
permissions: userPermission,
},
})
}
Expand All @@ -56,21 +50,9 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<

const workspaceId = id

// Check if user is a member with appropriate permissions
const membership = await db
.select()
.from(workspaceMember)
.where(
and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, session.user.id))
)
.then((rows) => rows[0])

if (!membership) {
return NextResponse.json({ error: 'Workspace not found or access denied' }, { status: 404 })
}

// For now, only allow owners to update workspace
if (membership.role !== 'owner') {
// Check if user has admin permissions to update workspace
const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
if (userPermission !== 'admin') {
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
}

Expand Down Expand Up @@ -100,7 +82,7 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
return NextResponse.json({
workspace: {
...updatedWorkspace,
role: membership.role,
permissions: userPermission,
},
})
} catch (error) {
Expand All @@ -122,22 +104,23 @@ export async function DELETE(

const workspaceId = id

// Check if user is the owner
const membership = await db
.select()
.from(workspaceMember)
.where(
and(eq(workspaceMember.workspaceId, workspaceId), eq(workspaceMember.userId, session.user.id))
)
.then((rows) => rows[0])

if (!membership || membership.role !== 'owner') {
// Check if user has admin permissions to delete workspace
const userPermission = await getUserEntityPermissions(session.user.id, 'workspace', workspaceId)
if (userPermission !== 'admin') {
return NextResponse.json({ error: 'Insufficient permissions' }, { status: 403 })
}

try {
// Delete workspace (cascade will handle members)
await db.delete(workspace).where(eq(workspace.id, workspaceId))
// Use a transaction to ensure data consistency
await db.transaction(async (tx) => {
// 1. Delete all permissions associated with this workspace
await tx
.delete(permissions)
.where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId)))

// 2. Delete workspace (cascade will handle members, workflows, etc.)
await tx.delete(workspace).where(eq(workspace.id, workspaceId))
})

return NextResponse.json({ success: true })
} catch (error) {
Expand Down
54 changes: 37 additions & 17 deletions apps/sim/app/api/workspaces/invitations/accept/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/env'
import { db } from '@/db'
import { user, workspace, workspaceInvitation, workspaceMember } from '@/db/schema'
import { permissions, user, workspace, workspaceInvitation, workspaceMember } from '@/db/schema'

// Accept an invitation via token
export async function GET(req: NextRequest) {
Expand Down Expand Up @@ -153,24 +153,44 @@ export async function GET(req: NextRequest) {
)
}

// Add user to workspace
await db.insert(workspaceMember).values({
id: randomUUID(),
workspaceId: invitation.workspaceId,
userId: session.user.id,
role: invitation.role,
joinedAt: new Date(),
updatedAt: new Date(),
})

// Mark invitation as accepted
await db
.update(workspaceInvitation)
.set({
status: 'accepted',
// Add user to workspace, permissions, and mark invitation as accepted in a transaction
await db.transaction(async (tx) => {
// Add user to workspace
await tx.insert(workspaceMember).values({
id: randomUUID(),
workspaceId: invitation.workspaceId,
userId: session.user.id,
role: invitation.role,
joinedAt: new Date(),
updatedAt: new Date(),
})
.where(eq(workspaceInvitation.id, invitation.id))

// Create permissions for the user
const permissionsToInsert = [
{
id: randomUUID(),
entityType: 'workspace' as const,
entityId: invitation.workspaceId,
userId: session.user.id,
permissionType: invitation.permissions || 'read',
createdAt: new Date(),
updatedAt: new Date(),
},
]

if (permissionsToInsert.length > 0) {
await tx.insert(permissions).values(permissionsToInsert)
}
Comment on lines +181 to +183
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: This conditional check is unnecessary since permissionsToInsert is a static array with exactly one item. You can perform the insert directly.

Suggested change
if (permissionsToInsert.length > 0) {
await tx.insert(permissions).values(permissionsToInsert)
}
await tx.insert(permissions).values(permissionsToInsert)


// Mark invitation as accepted
await tx
.update(workspaceInvitation)
.set({
status: 'accepted',
updatedAt: new Date(),
})
.where(eq(workspaceInvitation.id, invitation.id))
})

// Redirect to the workspace
return NextResponse.redirect(
Expand Down
Loading