feat: add organization.passwordReset PBAC permission (#27377)
* feat: add organization.passwordReset PBAC permission Allow org admins/owners to reset passwords for members of their organization via a new PBAC permission. Previously this was only available to system-level admins. - Add PasswordReset to CustomAction enum and PERMISSION_REGISTRY - Create migration to grant permission to admin_role (owner has wildcard) - Add org-scoped tRPC endpoint using createOrgPbacProcedure - Handler validates org membership, prevents self-targeting, and blocks resetting owner passwords - Wire permission through MemberPermissions, getOrgMembersPageData, and the org members table UI dropdown Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use targeted select in org password reset to avoid over-fetching Replace findById with findForPasswordReset repository method that only selects email, name, and locale instead of the full userSelect which includes sensitive fields like twoFactorSecret and backupCodes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * test: add unit tests for sendPasswordReset handler Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
1f8f134854
commit
08a2b0bfb5
@@ -1,5 +1,3 @@
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
@@ -7,17 +5,17 @@ import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownMenuTrigger,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuPortal,
|
||||
DropdownItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/components/dropdown";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
import type { UserTableUser, UserTableAction } from "./types";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { UserTableAction, UserTableUser } from "./types";
|
||||
|
||||
export function TableActions({
|
||||
user,
|
||||
@@ -33,6 +31,7 @@ export function TableActions({
|
||||
canRemove: boolean;
|
||||
canImpersonate: boolean;
|
||||
canResendInvitation: boolean;
|
||||
canResetPassword: boolean;
|
||||
};
|
||||
}) {
|
||||
const { t, i18n } = useLocale();
|
||||
@@ -45,6 +44,14 @@ export function TableActions({
|
||||
showToast(error.message, "error");
|
||||
},
|
||||
});
|
||||
const sendPasswordResetMutation = trpc.viewer.organizations.sendPasswordReset.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("password_reset_email_sent"), "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error.message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const usersProfileUrl = `${domain}/${user.username}`;
|
||||
|
||||
@@ -96,26 +103,37 @@ export function TableActions({
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissionsForUser.canImpersonate && (
|
||||
<>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "SET_IMPERSONATE_ID",
|
||||
payload: {
|
||||
user,
|
||||
showModal: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
StartIcon="lock">
|
||||
{t("impersonate")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "SET_IMPERSONATE_ID",
|
||||
payload: {
|
||||
user,
|
||||
showModal: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
StartIcon="lock">
|
||||
{t("impersonate")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissionsForUser.canResetPassword && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
onClick={() => {
|
||||
sendPasswordResetMutation.mutate({ userId: user.id });
|
||||
}}
|
||||
StartIcon="key">
|
||||
{t("reset_password")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{(permissionsForUser.canImpersonate || permissionsForUser.canResetPassword) &&
|
||||
permissionsForUser.canRemove && <DropdownMenuSeparator />}
|
||||
{permissionsForUser.canRemove && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
@@ -170,24 +188,34 @@ export function TableActions({
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
{permissionsForUser.canEdit && (
|
||||
<>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "EDIT_USER_SHEET",
|
||||
payload: {
|
||||
user,
|
||||
showModal: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
StartIcon="pencil">
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "EDIT_USER_SHEET",
|
||||
payload: {
|
||||
user,
|
||||
showModal: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
StartIcon="pencil">
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissionsForUser.canResetPassword && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
onClick={() => {
|
||||
sendPasswordResetMutation.mutate({ userId: user.id });
|
||||
}}
|
||||
StartIcon="key">
|
||||
{t("reset_password")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{permissionsForUser.canRemove && (
|
||||
<DropdownMenuItem>
|
||||
|
||||
Reference in New Issue
Block a user