feat: clear all permission overrides
This commit is contained in:
@@ -1,13 +1,17 @@
|
|||||||
import type { NextPageWithLayout } from "~/pages/_app";
|
import type { NextPageWithLayout } from "~/pages/_app";
|
||||||
import { getDashboardLayout } from "~/components/Dashboard";
|
import { getDashboardLayout } from "~/components/Dashboard";
|
||||||
import { SettingsLayout } from "~/components/SettingsLayout";
|
import { SettingsLayout } from "~/components/SettingsLayout";
|
||||||
|
import Popup from "~/components/Popup";
|
||||||
import PermissionsSettings from "~/views/settings/PermissionsSettings";
|
import PermissionsSettings from "~/views/settings/PermissionsSettings";
|
||||||
|
|
||||||
const PermissionsSettingsPage: NextPageWithLayout = () => {
|
const PermissionsSettingsPage: NextPageWithLayout = () => {
|
||||||
return (
|
return (
|
||||||
<SettingsLayout currentTab="permissions">
|
<>
|
||||||
<PermissionsSettings />
|
<SettingsLayout currentTab="permissions">
|
||||||
</SettingsLayout>
|
<PermissionsSettings />
|
||||||
|
</SettingsLayout>
|
||||||
|
<Popup />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,47 @@
|
|||||||
import { t } from "@lingui/core/macro";
|
import { t } from "@lingui/core/macro";
|
||||||
|
|
||||||
import { PageHead } from "~/components/PageHead";
|
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 { useWorkspace } from "~/providers/workspace";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
import { ClearCustomPermissionsConfirmation } from "./components/ClearCustomPermissionsConfirmation";
|
||||||
import { RolePermissions } from "./components/RolePermissions";
|
import { RolePermissions } from "./components/RolePermissions";
|
||||||
|
|
||||||
export default function PermissionsSettings() {
|
export default function PermissionsSettings() {
|
||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
|
const { openModal, isOpen, modalContentType } = useModal();
|
||||||
|
const { showPopup } = usePopup();
|
||||||
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const isAdmin = workspace.role === "admin";
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHead title={t`Settings | Permissions`} />
|
<PageHead title={t`Settings | Permissions`} />
|
||||||
@@ -22,13 +55,45 @@ export default function PermissionsSettings() {
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isAdmin ? (
|
{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">
|
<p className="mt-4 text-sm text-neutral-500 dark:text-dark-900">
|
||||||
{t`You need to be an admin to manage workspace permissions.`}
|
{t`You need to be an admin to manage workspace permissions.`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
modalSize="sm"
|
||||||
|
isVisible={isOpen && modalContentType === "CLEAR_CUSTOM_PERMISSIONS"}
|
||||||
|
>
|
||||||
|
<ClearCustomPermissionsConfirmation
|
||||||
|
resetAllOverrides={resetAllOverrides}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -350,6 +350,55 @@ export const permissionRouter = createTRPCRouter({
|
|||||||
|
|
||||||
await permissionRepo.clearMemberPermissionOverrides(ctx.db, member.id);
|
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 };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
getWorkspaceRoles: protectedProcedure
|
getWorkspaceRoles: protectedProcedure
|
||||||
|
|||||||
@@ -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 type { dbClient } from "@kan/db/client";
|
||||||
import {
|
import {
|
||||||
@@ -260,6 +260,32 @@ export const clearMemberPermissionOverrides = async (
|
|||||||
.where(eq(workspaceMemberPermissions.workspaceMemberId, workspaceMemberId));
|
.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
|
* Get member with their role by userId and workspaceId
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user