feat: clear all permission overrides

This commit is contained in:
Henry
2026-01-28 23:18:44 +00:00
parent 95bfe41910
commit 62725ffa38
5 changed files with 204 additions and 5 deletions

View File

@@ -1,13 +1,17 @@
import type { NextPageWithLayout } from "~/pages/_app";
import { getDashboardLayout } from "~/components/Dashboard";
import { SettingsLayout } from "~/components/SettingsLayout";
import Popup from "~/components/Popup";
import PermissionsSettings from "~/views/settings/PermissionsSettings";
const PermissionsSettingsPage: NextPageWithLayout = () => {
return (
<SettingsLayout currentTab="permissions">
<PermissionsSettings />
</SettingsLayout>
<>
<SettingsLayout currentTab="permissions">
<PermissionsSettings />
</SettingsLayout>
<Popup />
</>
);
};

View File

@@ -1,14 +1,47 @@
import { t } from "@lingui/core/macro";
import { PageHead } from "~/components/PageHead";
import Button from "~/components/Button";
import Modal from "~/components/modal";
import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
import { ClearCustomPermissionsConfirmation } from "./components/ClearCustomPermissionsConfirmation";
import { RolePermissions } from "./components/RolePermissions";
export default function PermissionsSettings() {
const { workspace } = useWorkspace();
const { openModal, isOpen, modalContentType } = useModal();
const { showPopup } = usePopup();
const utils = api.useUtils();
const isAdmin = workspace.role === "admin";
const resetAllOverrides = api.permission.resetWorkspaceMemberPermissions.useMutation(
{
onSuccess: async () => {
showPopup({
header: t`Overrides cleared`,
message: t`All member permission overrides have been reset to their role defaults.`,
icon: "success",
});
// Refresh any relevant workspace data
await utils.workspace.byId.invalidate({
workspacePublicId: workspace.publicId,
});
},
onError: () => {
showPopup({
header: t`Unable to clear overrides`,
message: t`Please try again later, or contact customer support.`,
icon: "error",
});
},
},
);
return (
<>
<PageHead title={t`Settings | Permissions`} />
@@ -22,13 +55,45 @@ export default function PermissionsSettings() {
</p>
{isAdmin ? (
<RolePermissions />
<>
<RolePermissions />
<div className="mt-8">
<h2 className="mb-4 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
{t`Custom permissions`}
</h2>
<p className="mb-6 text-sm text-neutral-500 dark:text-dark-900">
{t`Clear any custom member permissions so that all members only inherit permissions from their role defaults.`}
</p>
<Button
variant="secondary"
size="sm"
onClick={() => {
if (!workspace.publicId || resetAllOverrides.isPending) {
return;
}
openModal("CLEAR_CUSTOM_PERMISSIONS");
}}
disabled={resetAllOverrides.isPending}
>
{t`Clear custom permissions`}
</Button>
</div>
</>
) : (
<p className="mt-4 text-sm text-neutral-500 dark:text-dark-900">
{t`You need to be an admin to manage workspace permissions.`}
</p>
)}
</div>
<Modal
modalSize="sm"
isVisible={isOpen && modalContentType === "CLEAR_CUSTOM_PERMISSIONS"}
>
<ClearCustomPermissionsConfirmation
resetAllOverrides={resetAllOverrides}
/>
</Modal>
</>
);
}

View File

@@ -0,0 +1,55 @@
import { t } from "@lingui/core/macro";
import Button from "~/components/Button";
import { useModal } from "~/providers/modal";
import { useWorkspace } from "~/providers/workspace";
import type { api } from "~/utils/api";
type ResetMutation = ReturnType<
typeof api.permission.resetWorkspaceMemberPermissions.useMutation
>;
export function ClearCustomPermissionsConfirmation({
resetAllOverrides,
}: {
resetAllOverrides: ResetMutation;
}) {
const { closeModal } = useModal();
const { workspace } = useWorkspace();
const handleConfirm = () => {
if (!workspace.publicId || resetAllOverrides.isPending) return;
resetAllOverrides.mutate({
workspacePublicId: workspace.publicId,
});
closeModal();
};
return (
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
{t`Clear all custom permissions?`}
</h2>
<p className="mb-4 text-sm text-light-900 dark:text-dark-900">
{t`This will remove all custom member permissions in this workspace. Members will inherit permissions only from their roles.`}
</p>
</div>
<div className="mt-5 flex justify-end space-x-2 sm:mt-6">
<Button size="sm" variant="secondary" onClick={() => closeModal()}>
{t`Cancel`}
</Button>
<Button
size="sm"
variant="primary"
onClick={handleConfirm}
isLoading={resetAllOverrides.isPending}
>
{t`Clear custom permissions`}
</Button>
</div>
</div>
);
}

View File

@@ -350,6 +350,55 @@ export const permissionRouter = createTRPCRouter({
await permissionRepo.clearMemberPermissionOverrides(ctx.db, member.id);
return { success: true };
}),
resetWorkspaceMemberPermissions: protectedProcedure
.meta({
openapi: {
summary: "Reset all member permission overrides in a workspace",
method: "POST",
path: "/workspaces/{workspacePublicId}/members/permissions/reset",
description:
"Clears all custom permission overrides for all members in a workspace so their effective permissions come only from their roles",
tags: ["Permissions"],
protect: true,
},
})
.input(
z.object({
workspacePublicId: 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");
await permissionRepo.clearAllMemberPermissionOverridesForWorkspace(
ctx.db,
workspace.id,
);
return { success: true };
}),
getWorkspaceRoles: protectedProcedure

View File

@@ -1,4 +1,4 @@
import { and, eq, isNull } from "drizzle-orm";
import { and, eq, isNull, inArray } from "drizzle-orm";
import type { dbClient } from "@kan/db/client";
import {
@@ -260,6 +260,32 @@ export const clearMemberPermissionOverrides = async (
.where(eq(workspaceMemberPermissions.workspaceMemberId, workspaceMemberId));
};
/**
* Clear all permission overrides for all members in a workspace
*/
export const clearAllMemberPermissionOverridesForWorkspace = async (
db: dbClient,
workspaceId: number,
) => {
const memberIds = await db
.select({ id: workspaceMembers.id })
.from(workspaceMembers)
.where(
and(
eq(workspaceMembers.workspaceId, workspaceId),
isNull(workspaceMembers.deletedAt),
),
);
if (memberIds.length === 0) return;
const ids = memberIds.map((m) => m.id);
await db
.delete(workspaceMemberPermissions)
.where(inArray(workspaceMemberPermissions.workspaceMemberId, ids));
};
/**
* Get member with their role by userId and workspaceId
*/