From 95bfe41910719c99db878d6c9291d8363cb88fdb Mon Sep 17 00:00:00 2001 From: Henry Date: Wed, 28 Jan 2026 23:00:10 +0000 Subject: [PATCH] feat: reset role defaults --- .../components/EditMemberPermissionsModal.tsx | 128 ++++++++++++------ packages/api/src/routers/permission.ts | 61 +++++++++ packages/db/src/repository/permission.repo.ts | 12 ++ 3 files changed, 161 insertions(+), 40 deletions(-) diff --git a/apps/web/src/views/members/components/EditMemberPermissionsModal.tsx b/apps/web/src/views/members/components/EditMemberPermissionsModal.tsx index 917cdcc0..3db60ef7 100644 --- a/apps/web/src/views/members/components/EditMemberPermissionsModal.tsx +++ b/apps/web/src/views/members/components/EditMemberPermissionsModal.tsx @@ -73,8 +73,34 @@ export function EditMemberPermissionsModal() { }, }); + const resetMutation = api.permission.resetMemberPermissions.useMutation({ + onSuccess: async () => { + showPopup({ + header: t`Permissions reset`, + message: t`This member's permissions have been reset to their role defaults.`, + icon: "success", + }); + + await utils.permission.getMemberPermissions.invalidate({ + workspacePublicId: workspace.publicId, + memberPublicId: entityId, + }); + }, + onError: () => { + showPopup({ + header: t`Unable to reset permissions`, + message: t`Please try again later, or contact customer support.`, + icon: "error", + }); + }, + }); + const effectivePermissions = (data?.permissions ?? []) as Permission[]; - const isBusy = grantMutation.isPending || revokeMutation.isPending; + const hasOverrides = (data?.overrides?.length ?? 0) > 0; + const isBusy = + grantMutation.isPending || + revokeMutation.isPending || + resetMutation.isPending; const handleToggle = (permission: Permission, nextState: boolean) => { if (!workspace.publicId || !entityId) return; @@ -152,47 +178,69 @@ export function EditMemberPermissionsModal() { {t`Loading permissions...`}

) : ( -
- {Object.values(permissionCategories).map((category, index) => ( -
0 ? "border-t border-light-300 dark:border-dark-300" : ""}`} - > -
- {category.label} -
-
- {category.permissions.map((permission) => { - const label = - permissionLabels[permission] ?? (permission as string); + <> +
+ {Object.values(permissionCategories).map((category, index) => ( +
0 + ? "border-t border-light-300 dark:border-dark-300" + : "" + }`} + > +
+ {category.label} +
+
+ {category.permissions.map((permission) => { + const label = + permissionLabels[permission] ?? (permission as string); - return ( -
- - {label} - - - handleToggle( - permission, - !effectivePermissions.includes(permission), - ) - } - /> -
- ); - })} + return ( +
+ + {label} + + + handleToggle( + permission, + !effectivePermissions.includes(permission), + ) + } + /> +
+ ); + })} +
-
- ))} -
+ ))} +
+
+ +
+ )}
diff --git a/packages/api/src/routers/permission.ts b/packages/api/src/routers/permission.ts index 48ad28a5..45397950 100644 --- a/packages/api/src/routers/permission.ts +++ b/packages/api/src/routers/permission.ts @@ -289,6 +289,67 @@ export const permissionRouter = createTRPCRouter({ input.permission as Permission, ); + return { success: true }; + }), + resetMemberPermissions: protectedProcedure + .meta({ + openapi: { + summary: "Reset member permissions to role defaults", + method: "POST", + path: "/workspaces/{workspacePublicId}/members/{memberPublicId}/permissions/reset", + description: + "Clears all custom permission overrides for a member so their effective permissions come only from their role", + tags: ["Permissions"], + protect: true, + }, + }) + .input( + z.object({ + workspacePublicId: z.string().min(12), + memberPublicId: z.string().min(12), + }), + ) + .output(z.object({ success: z.boolean() })) + .mutation(async ({ ctx, input }) => { + const userId = ctx.user?.id; + + if (!userId) { + throw new TRPCError({ + message: "User not authenticated", + code: "UNAUTHORIZED", + }); + } + + const workspace = await workspaceRepo.getByPublicId( + ctx.db, + input.workspacePublicId, + ); + + if (!workspace) { + throw new TRPCError({ + message: "Workspace not found", + code: "NOT_FOUND", + }); + } + + await assertPermission(ctx.db, userId, workspace.id, "member:edit"); + + const member = await memberRepo.getByPublicId( + ctx.db, + input.memberPublicId, + ); + + if (!member) { + throw new TRPCError({ + message: "Member not found", + code: "NOT_FOUND", + }); + } + + await assertCanManageMember(ctx.db, userId, workspace.id, member.id); + + await permissionRepo.clearMemberPermissionOverrides(ctx.db, member.id); + return { success: true }; }), getWorkspaceRoles: protectedProcedure diff --git a/packages/db/src/repository/permission.repo.ts b/packages/db/src/repository/permission.repo.ts index e64020dc..c940db55 100644 --- a/packages/db/src/repository/permission.repo.ts +++ b/packages/db/src/repository/permission.repo.ts @@ -248,6 +248,18 @@ export const revokePermission = async ( return result; }; +/** + * Clear all permission overrides for a workspace member + */ +export const clearMemberPermissionOverrides = async ( + db: dbClient, + workspaceMemberId: number, +) => { + await db + .delete(workspaceMemberPermissions) + .where(eq(workspaceMemberPermissions.workspaceMemberId, workspaceMemberId)); +}; + /** * Get member with their role by userId and workspaceId */