feat: reset role defaults

This commit is contained in:
Henry
2026-01-28 23:00:10 +00:00
parent 3369092509
commit 95bfe41910
3 changed files with 161 additions and 40 deletions

View File

@@ -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 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) => { const handleToggle = (permission: Permission, nextState: boolean) => {
if (!workspace.publicId || !entityId) return; if (!workspace.publicId || !entityId) return;
@@ -152,11 +178,16 @@ export function EditMemberPermissionsModal() {
{t`Loading permissions...`} {t`Loading permissions...`}
</p> </p>
) : ( ) : (
<>
<div className="max-h-80 space-y-3 overflow-y-auto pr-1"> <div className="max-h-80 space-y-3 overflow-y-auto pr-1">
{Object.values(permissionCategories).map((category, index) => ( {Object.values(permissionCategories).map((category, index) => (
<div <div
key={category.label} key={category.label}
className={`py-2 ${index > 0 ? "border-t border-light-300 dark:border-dark-300" : ""}`} className={`py-2 ${
index > 0
? "border-t border-light-300 dark:border-dark-300"
: ""
}`}
> >
<div className="my-2 text-[12px] font-semibold text-light-900 dark:text-dark-950"> <div className="my-2 text-[12px] font-semibold text-light-900 dark:text-dark-950">
{category.label} {category.label}
@@ -171,7 +202,7 @@ export function EditMemberPermissionsModal() {
key={permission} key={permission}
className="flex items-center justify-between gap-3 py-0.5" className="flex items-center justify-between gap-3 py-0.5"
> >
<span className="text-xs text-light-900 dark:text-dark-900 mb-"> <span className="text-xs text-light-900 dark:text-dark-900">
{label} {label}
</span> </span>
<Toggle <Toggle
@@ -193,6 +224,23 @@ export function EditMemberPermissionsModal() {
</div> </div>
))} ))}
</div> </div>
<div className="mt-4 flex justify-end">
<button
type="button"
onClick={() => {
if (!workspace.publicId || !entityId || isBusy) return;
resetMutation.mutate({
workspacePublicId: workspace.publicId,
memberPublicId: entityId,
});
}}
disabled={isBusy || !hasOverrides}
className="rounded-md border border-light-400 px-3 py-1.5 text-xs text-light-900 hover:bg-light-200 disabled:opacity-60 dark:border-dark-400 dark:text-dark-900 dark:hover:bg-dark-200"
>
{t`Reset to role defaults`}
</button>
</div>
</>
)} )}
</div> </div>

View File

@@ -289,6 +289,67 @@ export const permissionRouter = createTRPCRouter({
input.permission as Permission, 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 }; return { success: true };
}), }),
getWorkspaceRoles: protectedProcedure getWorkspaceRoles: protectedProcedure

View File

@@ -248,6 +248,18 @@ export const revokePermission = async (
return result; 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 * Get member with their role by userId and workspaceId
*/ */