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
*/