From 62725ffa3807c414a346b06fabdca5e2ed28d67e Mon Sep 17 00:00:00 2001
From: Henry
Date: Wed, 28 Jan 2026 23:18:44 +0000
Subject: [PATCH] feat: clear all permission overrides
---
apps/web/src/pages/settings/permissions.tsx | 10 ++-
.../views/settings/PermissionsSettings.tsx | 67 ++++++++++++++++++-
.../ClearCustomPermissionsConfirmation.tsx | 55 +++++++++++++++
packages/api/src/routers/permission.ts | 49 ++++++++++++++
packages/db/src/repository/permission.repo.ts | 28 +++++++-
5 files changed, 204 insertions(+), 5 deletions(-)
create mode 100644 apps/web/src/views/settings/components/ClearCustomPermissionsConfirmation.tsx
diff --git a/apps/web/src/pages/settings/permissions.tsx b/apps/web/src/pages/settings/permissions.tsx
index b4364347..17676dd9 100644
--- a/apps/web/src/pages/settings/permissions.tsx
+++ b/apps/web/src/pages/settings/permissions.tsx
@@ -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 (
-
-
-
+ <>
+
+
+
+
+ >
);
};
diff --git a/apps/web/src/views/settings/PermissionsSettings.tsx b/apps/web/src/views/settings/PermissionsSettings.tsx
index 01c30138..10e98750 100644
--- a/apps/web/src/views/settings/PermissionsSettings.tsx
+++ b/apps/web/src/views/settings/PermissionsSettings.tsx
@@ -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 (
<>
@@ -22,13 +55,45 @@ export default function PermissionsSettings() {
{isAdmin ? (
-
+ <>
+
+
+
+ {t`Custom permissions`}
+
+
+ {t`Clear any custom member permissions so that all members only inherit permissions from their role defaults.`}
+
+
+
+ >
) : (
{t`You need to be an admin to manage workspace permissions.`}
)}
+
+
+
+
>
);
}
diff --git a/apps/web/src/views/settings/components/ClearCustomPermissionsConfirmation.tsx b/apps/web/src/views/settings/components/ClearCustomPermissionsConfirmation.tsx
new file mode 100644
index 00000000..5f505091
--- /dev/null
+++ b/apps/web/src/views/settings/components/ClearCustomPermissionsConfirmation.tsx
@@ -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 (
+
+
+
+ {t`Clear all custom permissions?`}
+
+
+ {t`This will remove all custom member permissions in this workspace. Members will inherit permissions only from their roles.`}
+
+
+
+
+
+
+
+ );
+}
+
+
diff --git a/packages/api/src/routers/permission.ts b/packages/api/src/routers/permission.ts
index 45397950..f62e1082 100644
--- a/packages/api/src/routers/permission.ts
+++ b/packages/api/src/routers/permission.ts
@@ -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
diff --git a/packages/db/src/repository/permission.repo.ts b/packages/db/src/repository/permission.repo.ts
index c940db55..9b0cb2ce 100644
--- a/packages/db/src/repository/permission.repo.ts
+++ b/packages/db/src/repository/permission.repo.ts
@@ -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
*/