Files
sean-brydonandClaude Opus 4.5 08a2b0bfb5 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>
2026-01-29 11:18:02 +00:00

63 lines
1.8 KiB
TypeScript

import { passwordResetRequest } from "@calcom/features/auth/lib/passwordResetRequest";
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { TrpcSessionUser } from "../../../types";
import type { TOrgPasswordResetSchema } from "./sendPasswordReset.schema";
type SendPasswordResetOptions = {
ctx: {
user: NonNullable<TrpcSessionUser>;
organizationId: number;
};
input: TOrgPasswordResetSchema;
};
const sendPasswordResetHandler = async ({ ctx, input }: SendPasswordResetOptions) => {
const { organizationId } = ctx;
const { userId } = input;
if (userId === ctx.user.id) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "You cannot reset your own password through this endpoint.",
});
}
const membershipRepository = new MembershipRepository();
const targetMembership = await membershipRepository.findUniqueByUserIdAndTeamId({
userId,
teamId: organizationId,
});
if (!targetMembership) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User is not a member of this organization.",
});
}
if (targetMembership.role === MembershipRole.OWNER) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Cannot reset password for an organization owner.",
});
}
const userRepository = new UserRepository(prisma);
const user = await userRepository.findForPasswordReset({ id: userId });
if (!user) {
throw new TRPCError({ code: "NOT_FOUND", message: "User not found." });
}
await passwordResetRequest(user);
return { success: true };
};
export default sendPasswordResetHandler;