Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { redirect } from "next/navigation";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import WebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
import { APP_NAME } from "@calcom/lib/constants";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { webhookRouter } from "@calcom/trpc/server/routers/viewer/webhook/_router";

import { buildLegacyRequest } from "@lib/buildLegacyCtx";
Expand All @@ -26,11 +25,10 @@ const WebhooksViewServerWrapper = async () => {
redirect("/auth/login");
}

const isAdmin = session.user.role === UserPermissionRole.ADMIN;
const caller = await createRouterCaller(webhookRouter);
const data = await caller.getByViewer();

return <WebhooksView data={data} isAdmin={isAdmin} />;
return <WebhooksView data={data} />;
};

export default WebhooksViewServerWrapper;
5 changes: 5 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -3498,6 +3498,11 @@
"pbac_desc_view_workflows": "View existing workflows and their configurations",
"pbac_desc_update_workflows": "Edit and modify workflow settings",
"pbac_desc_delete_workflows": "Remove workflows from the system",
"pbac_resource_webhook": "Webhook",
"pbac_desc_create_webhooks": "Create webhooks",
"pbac_desc_view_webhooks": "View webhooks",
"pbac_desc_update_webhooks": "Update webhooks",
"pbac_desc_delete_webhooks": "Delete webhooks",
"pbac_desc_manage_workflows": "Full management access to all workflows",
"pbac_desc_create_event_types": "Create event types",
"pbac_desc_view_event_types": "View event types",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,11 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
setEditModalOpen(true);
setWebhookToEdit(webhook);
}}
// TODO (SEAN): Implement Permissions here when we have event-types PR merged
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to fix this (leaving a comment for visibility)

permissions={{
canEditWebhook: !webhookLockedStatus.disabled,
canDeleteWebhook: !webhookLockedStatus.disabled,
}}
/>
);
})}
Expand Down
33 changes: 33 additions & 0 deletions packages/features/pbac/domain/types/permission-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum Resource {
Role = "role",
RoutingForm = "routingForm",
Workflow = "workflow",
Webhook = "webhook",
}

export enum CrudAction {
Expand Down Expand Up @@ -516,4 +517,36 @@ export const PERMISSION_REGISTRY: PermissionRegistry = {
dependsOn: ["routingForm.read"],
},
},
[Resource.Webhook]: {
_resource: {
i18nKey: "pbac_resource_webhook",
},
[CrudAction.Create]: {
description: "Create webhooks",
category: "webhook",
i18nKey: "pbac_action_create",
descriptionI18nKey: "pbac_desc_create_webhooks",
dependsOn: ["webhook.read"],
},
[CrudAction.Read]: {
description: "View webhooks",
category: "webhook",
i18nKey: "pbac_action_read",
descriptionI18nKey: "pbac_desc_view_webhooks",
},
[CrudAction.Update]: {
description: "Update webhooks",
category: "webhook",
i18nKey: "pbac_action_update",
descriptionI18nKey: "pbac_desc_update_webhooks",
dependsOn: ["webhook.read"],
},
[CrudAction.Delete]: {
description: "Delete webhooks",
category: "webhook",
i18nKey: "pbac_action_delete",
descriptionI18nKey: "pbac_desc_delete_webhooks",
dependsOn: ["webhook.read"],
},
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { useRouter } from "next/navigation";

import { CreateButtonWithTeamsList } from "@calcom/features/ee/teams/components/createButton/CreateButtonWithTeamsList";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole } from "@calcom/prisma/enums";

export const CreateNewWebhookButton = ({ isAdmin }: { isAdmin: boolean }) => {
export const CreateNewWebhookButton = () => {
const router = useRouter();
const { t } = useLocale();
const createFunction = (teamId?: number, platform?: boolean) => {
Expand All @@ -20,10 +21,13 @@ export const CreateNewWebhookButton = ({ isAdmin }: { isAdmin: boolean }) => {
<CreateButtonWithTeamsList
color="secondary"
subtitle={t("create_for").toUpperCase()}
isAdmin={isAdmin}
createFunction={createFunction}
data-testid="new_webhook"
includeOrg={true}
withPermission={{
permission: "webhook.create",
fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],
}}
/>
);
};
Expand Down
69 changes: 40 additions & 29 deletions packages/features/webhooks/components/WebhookListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@ export default function WebhookListItem(props: {
canEditWebhook?: boolean;
onEditWebhook: () => void;
lastItem: boolean;
readOnly?: boolean;
permissions: {
canEditWebhook?: boolean;
canDeleteWebhook?: boolean;
};
}) {
const { t } = useLocale();
const utils = trpc.useUtils();
const { webhook } = props;
const canEditWebhook = props.canEditWebhook ?? true;

const deleteWebhook = trpc.viewer.webhook.delete.useMutation({
async onSuccess() {
Expand Down Expand Up @@ -87,7 +89,7 @@ export default function WebhookListItem(props: {
{webhook.subscriberUrl}
</p>
</Tooltip>
{!!props.readOnly && (
{!props.permissions.canEditWebhook && (
<Badge variant="gray" className="ml-2 ">
{t("readonly")}
</Badge>
Expand All @@ -107,12 +109,12 @@ export default function WebhookListItem(props: {
</div>
</Tooltip>
</div>
{!props.readOnly && (
{(props.permissions.canEditWebhook || props.permissions.canDeleteWebhook) && (
<div className="ml-2 flex items-center space-x-4">
<Switch
defaultChecked={webhook.active}
data-testid="webhook-switch"
disabled={!canEditWebhook}
disabled={!props.permissions.canEditWebhook}
onCheckedChange={() =>
toggleWebhook.mutate({
id: webhook.id,
Expand All @@ -123,39 +125,48 @@ export default function WebhookListItem(props: {
}
/>

<Button
className="hidden lg:flex"
color="secondary"
onClick={props.onEditWebhook}
data-testid="webhook-edit-button">
{t("edit")}
</Button>
{props.permissions.canEditWebhook && (
<Button
className="hidden lg:flex"
color="secondary"
onClick={props.onEditWebhook}
data-testid="webhook-edit-button">
{t("edit")}
</Button>
)}

<Button
className="hidden lg:flex"
color="destructive"
StartIcon="trash"
variant="icon"
onClick={onDeleteWebhook}
/>
{props.permissions.canDeleteWebhook && (
<Button
className="hidden lg:flex"
color="destructive"
StartIcon="trash"
variant="icon"
onClick={onDeleteWebhook}
/>
)}

<Dropdown>
<DropdownMenuTrigger asChild>
<Button className="lg:hidden" StartIcon="ellipsis" variant="icon" color="secondary" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem StartIcon="pencil" color="secondary" onClick={props.onEditWebhook}>
{t("edit")}
</DropdownItem>
</DropdownMenuItem>
{props.permissions.canEditWebhook && (
<DropdownMenuItem>
<DropdownItem StartIcon="pencil" color="secondary" onClick={props.onEditWebhook}>
{t("edit")}
</DropdownItem>
</DropdownMenuItem>
)}

<DropdownMenuSeparator />

<DropdownMenuItem>
<DropdownItem StartIcon="trash" color="destructive" onClick={onDeleteWebhook}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
{props.permissions.canDeleteWebhook && (
<DropdownMenuItem>
<DropdownItem StartIcon="trash" color="destructive" onClick={onDeleteWebhook}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</Dropdown>
</div>
Expand Down
27 changes: 12 additions & 15 deletions packages/features/webhooks/pages/webhooks-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,33 +7,27 @@ import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import type { WebhooksByViewer } from "@calcom/trpc/server/routers/viewer/webhook/getByViewer.handler";
import classNames from "@calcom/ui/classNames";
import { Avatar } from "@calcom/ui/components/avatar";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";

import { WebhookListItem, CreateNewWebhookButton } from "../components";

type WebhooksByViewer = RouterOutputs["viewer"]["webhook"]["getByViewer"];

type Props = {
data: RouterOutputs["viewer"]["webhook"]["getByViewer"];
isAdmin: boolean;
data: WebhooksByViewer;
};

const WebhooksView = ({ data, isAdmin }: Props) => {
const WebhooksView = ({ data }: Props) => {
return (
<div>
<WebhooksList webhooksByViewer={data} isAdmin={isAdmin} />
<WebhooksList webhooksByViewer={data} />
</div>
);
};

const WebhooksList = ({
webhooksByViewer,
isAdmin,
}: {
webhooksByViewer: WebhooksByViewer;
isAdmin: boolean;
}) => {
const WebhooksList = ({ webhooksByViewer }: { webhooksByViewer: WebhooksByViewer }) => {
const { t } = useLocale();
const router = useRouter();
const { profiles, webhookGroups } = webhooksByViewer;
Expand All @@ -45,7 +39,7 @@ const WebhooksList = ({
<SettingsHeader
title={t("webhooks")}
description={t("add_webhook_description", { appName: APP_NAME })}
CTA={webhooksByViewer.webhookGroups.length > 0 ? <CreateNewWebhookButton isAdmin={isAdmin} /> : null}
CTA={webhooksByViewer.webhookGroups.length > 0 ? <CreateNewWebhookButton /> : null}
borderInShellHeader={false}>
{!!webhookGroups.length ? (
<div className={classNames("mt-6")}>
Expand All @@ -70,8 +64,11 @@ const WebhooksList = ({
<WebhookListItem
key={webhook.id}
webhook={webhook}
readOnly={group.metadata?.readOnly ?? false}
lastItem={group.webhooks.length === index + 1}
permissions={{
canEditWebhook: group?.metadata?.canModify ?? false,
canDeleteWebhook: group?.metadata?.canDelete ?? false,
}}
Comment on lines +68 to +71
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Mapping server metadata to UI permissions — ensure backend populates these

UI derives canEdit/canDelete from group.metadata.canModify/canDelete. Please confirm list/get handlers set these via PBAC checks.

Run:


🏁 Script executed:

#!/bin/bash
rg -nP --type=ts -C3 'getByViewer\.handler|list\.handler' packages/trpc/server/routers/viewer/webhook
rg -nP --type=ts -C3 '\bmetadata\b.*(canModify|canDelete)' packages/trpc/server/routers/viewer/webhook

Length of output: 2781


🏁 Script executed:

#!/bin/bash
rg -n -C3 'metadata' packages/trpc/server/routers/viewer/webhook/*.handler.ts
rg -n -C3 'canModify|canDelete' packages/trpc/server/routers/viewer/webhook/*.handler.ts

Length of output: 1731


🏁 Script executed:

#!/bin/bash
ls -1 packages/trpc/server/routers/viewer

Length of output: 350


🏁 Script executed:

#!/bin/bash
sed -n '1,200p' packages/trpc/server/routers/viewer/webhook/list.handler.ts

Length of output: 2417


Add canModify/canDelete metadata in list & getByViewer handlers
list.handler.ts & getByViewer.handler.ts don’t set metadata.canModify/metadata.canDelete—use PermissionCheckService to check "webhook.modify" and "webhook.delete" and include those booleans in each webhook’s metadata before returning.

🤖 Prompt for AI Agents
In packages/features/webhooks/pages/webhooks-view.tsx around lines 68 to 71, the
UI expects each webhook to have metadata.canModify and metadata.canDelete but
list.handler.ts and getByViewer.handler.ts do not populate those fields; update
both handlers to call PermissionCheckService (checking "webhook.modify" and
"webhook.delete") for each webhook and set metadata.canModify and
metadata.canDelete to the returned booleans on every webhook object before
returning the list/result so the front-end permissions prop is accurate.

onEditWebhook={() =>
router.push(`${WEBAPP_URL}/settings/developer/webhooks/${webhook.id}`)
}
Expand All @@ -88,7 +85,7 @@ const WebhooksList = ({
headline={t("create_your_first_webhook")}
description={t("create_your_first_webhook_description", { appName: APP_NAME })}
className="mt-6 rounded-b-lg"
buttonRaw={<CreateNewWebhookButton isAdmin={isAdmin} />}
buttonRaw={<CreateNewWebhookButton />}
border={true}
/>
)}
Expand Down
Loading
Loading