* 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>
63 lines
1.8 KiB
TypeScript
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;
|