-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Feature/else access control permissions #529
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 }) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * 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 }) | ||
| } | ||
| } | ||
| 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 | ||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
|
|
||
|
|
@@ -41,7 +35,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ | |
| return NextResponse.json({ | ||
| workspace: { | ||
| ...workspaceDetails, | ||
| role: membership.role, | ||
| permissions: userPermission, | ||
| }, | ||
| }) | ||
| } | ||
|
|
@@ -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 }) | ||
| } | ||
|
|
||
|
|
@@ -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) { | ||
|
|
@@ -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) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) { | ||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||
|
|
||||||||||
| // 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( | ||||||||||
|
|
||||||||||
There was a problem hiding this comment.
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