From 9538aebe5979e32c7fccab78604d46d030918910 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Fri, 11 Jul 2025 09:24:43 +0100 Subject: [PATCH 01/25] fix members page crash with pbac feature flag --- .../features/pbac/infrastructure/mappers/RoleOutputMapper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/features/pbac/infrastructure/mappers/RoleOutputMapper.ts b/packages/features/pbac/infrastructure/mappers/RoleOutputMapper.ts index 32f565c82b3340..4281f72951f904 100644 --- a/packages/features/pbac/infrastructure/mappers/RoleOutputMapper.ts +++ b/packages/features/pbac/infrastructure/mappers/RoleOutputMapper.ts @@ -11,7 +11,7 @@ export class RoleOutputMapper { description: prismaRole.description || undefined, teamId: prismaRole.teamId || undefined, type: prismaRole.type, - permissions: prismaRole.permissions.map(this.toDomainPermission), + permissions: prismaRole.permissions.map(RoleOutputMapper.toDomainPermission), createdAt: prismaRole.createdAt, updatedAt: prismaRole.updatedAt, }; @@ -28,6 +28,6 @@ export class RoleOutputMapper { } static toDomainList(prismaRoles: (PrismaRole & { permissions: PrismaRolePermission[] })[]): Role[] { - return prismaRoles.map(this.toDomain); + return prismaRoles.map(RoleOutputMapper.toDomain); } } From 36ea7247f9dedb0ac6d4f4e3ce4cdea99269e524 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Fri, 11 Jul 2025 10:05:51 +0100 Subject: [PATCH 02/25] invite member to org backend --- .../inviteMember/inviteMember.handler.ts | 19 ++++------ .../viewer/teams/inviteMember/utils.ts | 36 +++++++++++++++---- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts index f210d63ae46c33..c9759a66ebbca4 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts @@ -5,7 +5,6 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; -import { isOrganisationOwner } from "@calcom/lib/server/queries/organisations"; import { UserRepository } from "@calcom/lib/server/repository/user"; import prisma from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; @@ -18,7 +17,7 @@ import type { TInviteMemberInputSchema } from "./inviteMember.schema"; import type { TeamWithParent } from "./types"; import type { Invitation } from "./utils"; import { - ensureAtleastAdminPermissions, + ensureUserHasPermissions, findUsersWithInviteStatus, getOrgConnectionInfo, getOrgState, @@ -260,9 +259,9 @@ const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => { }); const isAddingNewOwner = !!invitations.find((invitation) => invitation.role === MembershipRole.OWNER); - if (isTeamAnOrg) { - await throwIfInviterCantAddOwnerToOrg(); - } + // if (isTeamAnOrg) { + // await throwIfInviterCantAddOwnerToOrg(); + // } if (isPlatform) { inviterOrgId = team.id; @@ -273,11 +272,12 @@ const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => { }); } - await ensureAtleastAdminPermissions({ + await ensureUserHasPermissions({ userId: inviter.id, - teamId: inviterOrgId && isInviterOrgAdmin ? inviterOrgId : input.teamId, + teamId: team.id, isOrg: isTeamAnOrg, }); + const result = await inviteMembersWithNoInviterPermissionCheck({ inviterName: inviter.name, team, @@ -287,11 +287,6 @@ const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => { invitations, }); return result; - - async function throwIfInviterCantAddOwnerToOrg() { - const isInviterOrgOwner = await isOrganisationOwner(inviter.id, input.teamId); - if (isAddingNewOwner && !isInviterOrgOwner) throw new TRPCError({ code: "UNAUTHORIZED" }); - } }; export default async function inviteMemberHandler({ ctx, input }: InviteMemberOptions) { diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index 14d44477ca0b8c..9977f49a6e86ea 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -4,6 +4,11 @@ import type { TFunction } from "i18next"; import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; import { sendTeamInviteEmail } from "@calcom/emails"; import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; +import { + RoleManagementError, + RoleManagementErrorCode, +} from "@calcom/features/pbac/domain/errors/role-management.error"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { ENABLE_PROFILE_SWITCHER, WEBAPP_URL } from "@calcom/lib/constants"; import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAnExistingUser"; @@ -57,6 +62,31 @@ type InvitableExistingUserWithProfile = InvitableExistingUser & { } | null; }; +export async function ensureUserHasPermissions({ + userId, + teamId, + isOrg, +}: { + userId: number; + teamId: number; + isOrg?: boolean; +}) { + const permissionCheckService = new PermissionCheckService(); + + const hasPermission = await permissionCheckService.checkPermission({ + userId, + teamId, + permission: isOrg ? "organization.invite" : "team.invite", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!hasPermission) + throw new RoleManagementError( + "You do not have permission to invite members.", + RoleManagementErrorCode.UNAUTHORIZED + ); +} + export async function ensureAtleastAdminPermissions({ userId, teamId, @@ -725,12 +755,6 @@ export const sendExistingUserTeamInviteEmails = async ({ await sendEmails(sendEmailsPromises); }; -type inviteMemberHandlerInput = { - teamId: number; - role?: "ADMIN" | "MEMBER" | "OWNER"; - language: string; -}; - export async function handleExistingUsersInvites({ invitableExistingUsers, team, From 403f7a7aeb54689bdc8017424ec3246231df1444 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Fri, 11 Jul 2025 11:49:39 +0100 Subject: [PATCH 03/25] update org permissions --- .../organizations/profile/page.tsx | 68 ++++++++++++++++++- .../organizations/pages/settings/profile.tsx | 15 ++-- .../viewer/organizations/update.handler.ts | 18 +++-- 3 files changed, 89 insertions(+), 12 deletions(-) diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx index 3849a833050756..968f20661ba71a 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx @@ -1,7 +1,19 @@ import { _generateMetadata, getTranslate } from "app/_utils"; +import { unstable_cache } from "next/cache"; +import { cookies, headers } from "next/headers"; +import { redirect } from "next/navigation"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile"; +import type { AppFlags } from "@calcom/features/flags/config"; +import { FeaturesRepository } from "@calcom/features/flags/features.repository"; +import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper"; +import { CrudAction, Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export const generateMetadata = async () => await _generateMetadata( @@ -12,15 +24,69 @@ export const generateMetadata = async () => "/settings/organizations/profile" ); +const getCachedTeamFeature = unstable_cache( + async (teamId: number, feature: keyof AppFlags) => { + const featureRepo = new FeaturesRepository(); + const res = await featureRepo.checkIfTeamHasFeature(teamId, feature); + return res; + }, + ["team-feature"], + { revalidate: 3600 } +); + +const getCachedResourcePermissions = unstable_cache( + async (userId: number, teamId: number, resource: Resource) => { + const permissionService = new PermissionCheckService(); + return permissionService.getResourcePermissions({ userId, teamId, resource }); + }, + ["resource-permissions"], + { revalidate: 3600 } +); + +const AdminOrOwnerRoles: MembershipRole[] = ["ADMIN", "OWNER"]; + const Page = async () => { + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); const t = await getTranslate(); + if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { + return redirect("/settings/profile"); + } + + const adminOrOwner = AdminOrOwnerRoles.includes(session.user.org?.role); + + let canEdit = adminOrOwner; + let canRead = adminOrOwner; + let canDelete = session.user.org.role === MembershipRole.OWNER; + + const pbacEnabled = await getCachedTeamFeature(session.user.profile.organizationId, "pbac"); + + if (pbacEnabled) { + const resourcePermissionsForUser = await getCachedResourcePermissions( + session.user.id, + session.user.profile.organizationId, + Resource.Organization + ); + + const roleActions = PermissionMapper.toActionMap(resourcePermissionsForUser, Resource.Organization); + + canRead = roleActions[CrudAction.Read] ?? false; + canEdit = roleActions[CrudAction.Update] ?? false; + canDelete = roleActions[CrudAction.Delete] ?? false; + } + return ( - + ); }; diff --git a/packages/features/ee/organizations/pages/settings/profile.tsx b/packages/features/ee/organizations/pages/settings/profile.tsx index a9ba9880c25031..3f64f380285553 100644 --- a/packages/features/ee/organizations/pages/settings/profile.tsx +++ b/packages/features/ee/organizations/pages/settings/profile.tsx @@ -7,7 +7,6 @@ import { useEffect, useLayoutEffect, useState } from "react"; import { Controller, useForm } from "react-hook-form"; import { z } from "zod"; -import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import OrgAppearanceViewWrapper from "@calcom/features/ee/organizations/pages/settings/appearance"; @@ -76,7 +75,15 @@ const SkeletonLoader = () => { ); }; -const OrgProfileView = () => { +const OrgProfileView = ({ + permissions, +}: { + permissions?: { + canRead: boolean; + canEdit: boolean; + canDelete: boolean; + }; +}) => { const { t } = useLocale(); const router = useRouter(); @@ -105,8 +112,6 @@ const OrgProfileView = () => { return ; } - const isOrgAdminOrOwner = checkAdminOrOwner(currentOrganisation.user.role); - const isBioEmpty = !currentOrganisation || !currentOrganisation.bio || @@ -128,7 +133,7 @@ const OrgProfileView = () => { return ( <> - {isOrgAdminOrOwner ? ( + {permissions?.canEdit ? ( <> diff --git a/packages/trpc/server/routers/viewer/organizations/update.handler.ts b/packages/trpc/server/routers/viewer/organizations/update.handler.ts index 5f1beff0e032f0..f394c95c712460 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.handler.ts @@ -1,13 +1,13 @@ import type { Prisma } from "@prisma/client"; +import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants"; import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers"; import { uploadLogo } from "@calcom/lib/server/avatar"; -import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations"; import { resizeBase64Image } from "@calcom/lib/server/resizeBase64Image"; import type { PrismaClient } from "@calcom/prisma"; import { prisma } from "@calcom/prisma"; -import { UserPermissionRole } from "@calcom/prisma/enums"; +import { MembershipRole } from "@calcom/prisma/enums"; import { teamMetadataSchema } from "@calcom/prisma/zod-utils"; import { TRPCError } from "@trpc/server"; @@ -109,12 +109,18 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { // A user can only have one org so we pass in their currentOrgId here const currentOrgId = ctx.user?.organization?.id || input.orgId; - const isUserOrganizationAdmin = currentOrgId && (await isOrganisationAdmin(ctx.user?.id, currentOrgId)); - const isUserRoleAdmin = ctx.user.role === UserPermissionRole.ADMIN; + if (!currentOrgId) throw new TRPCError({ code: "BAD_REQUEST", message: "Organization ID is required." }); - const isUserAuthorizedToUpdate = !!(isUserOrganizationAdmin || isUserRoleAdmin); + const permissionCheckService = new PermissionCheckService(); - if (!currentOrgId || !isUserAuthorizedToUpdate) throw new TRPCError({ code: "UNAUTHORIZED" }); + const isUserAuthorizedToUpdate = await permissionCheckService.checkPermission({ + userId: ctx.user?.id, + teamId: currentOrgId, + permission: "organization.update", + fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }); + + if (!isUserAuthorizedToUpdate) throw new TRPCError({ code: "UNAUTHORIZED" }); if (input.slug) { const userConflict = await prisma.team.findMany({ From 97cc7b89ce54d6aba7e68727772f4f5cd264c911 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Fri, 11 Jul 2025 12:31:59 +0100 Subject: [PATCH 04/25] feat: org profile update settings --- .../organizations/profile/page.tsx | 67 ++++----------- .../features/pbac/lib/resource-permissions.ts | 86 +++++++++++++++++++ 2 files changed, 105 insertions(+), 48 deletions(-) create mode 100644 packages/features/pbac/lib/resource-permissions.ts diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx index 968f20661ba71a..fcbe2c42507cab 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx @@ -1,15 +1,11 @@ import { _generateMetadata, getTranslate } from "app/_utils"; -import { unstable_cache } from "next/cache"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile"; -import type { AppFlags } from "@calcom/features/flags/config"; -import { FeaturesRepository } from "@calcom/features/flags/features.repository"; -import { PermissionMapper } from "@calcom/features/pbac/domain/mappers/PermissionMapper"; -import { CrudAction, Resource } from "@calcom/features/pbac/domain/types/permission-registry"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; import { MembershipRole } from "@calcom/prisma/enums"; @@ -24,27 +20,6 @@ export const generateMetadata = async () => "/settings/organizations/profile" ); -const getCachedTeamFeature = unstable_cache( - async (teamId: number, feature: keyof AppFlags) => { - const featureRepo = new FeaturesRepository(); - const res = await featureRepo.checkIfTeamHasFeature(teamId, feature); - return res; - }, - ["team-feature"], - { revalidate: 3600 } -); - -const getCachedResourcePermissions = unstable_cache( - async (userId: number, teamId: number, resource: Resource) => { - const permissionService = new PermissionCheckService(); - return permissionService.getResourcePermissions({ userId, teamId, resource }); - }, - ["resource-permissions"], - { revalidate: 3600 } -); - -const AdminOrOwnerRoles: MembershipRole[] = ["ADMIN", "OWNER"]; - const Page = async () => { const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); const t = await getTranslate(); @@ -53,27 +28,23 @@ const Page = async () => { return redirect("/settings/profile"); } - const adminOrOwner = AdminOrOwnerRoles.includes(session.user.org?.role); - - let canEdit = adminOrOwner; - let canRead = adminOrOwner; - let canDelete = session.user.org.role === MembershipRole.OWNER; - - const pbacEnabled = await getCachedTeamFeature(session.user.profile.organizationId, "pbac"); - - if (pbacEnabled) { - const resourcePermissionsForUser = await getCachedResourcePermissions( - session.user.id, - session.user.profile.organizationId, - Resource.Organization - ); - - const roleActions = PermissionMapper.toActionMap(resourcePermissionsForUser, Resource.Organization); - - canRead = roleActions[CrudAction.Read] ?? false; - canEdit = roleActions[CrudAction.Update] ?? false; - canDelete = roleActions[CrudAction.Delete] ?? false; - } + const { canRead, canEdit, canDelete } = await getResourcePermissions({ + userId: session.user.id, + teamId: session.user.profile.organizationId, + resource: Resource.Organization, + userRole: session.user.org.role, + fallbackRoles: { + read: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + delete: { + roles: [MembershipRole.OWNER], + }, + }, + }); return ( boolean; +} + +interface FallbackRoleConfig { + read?: RoleMapping; + create?: RoleMapping; + update?: RoleMapping; + delete?: RoleMapping; +} + +interface ResourcePermissionsOptions { + userId: number; + teamId: number; + resource: Resource; + userRole: MembershipRole; + fallbackRoles?: FallbackRoleConfig; +} + +interface ResourcePermissions { + canRead: boolean; + canEdit: boolean; + canDelete: boolean; + canCreate: boolean; +} + +const checkRoleAccess = (userRole: MembershipRole, mapping?: RoleMapping): boolean => { + if (!mapping) return false; + + const { roles, condition } = mapping; + const hasRole = roles.includes(userRole); + + if (condition) { + return hasRole && condition(userRole); + } + + return hasRole; +}; + +export const getResourcePermissions = async ({ + userId, + teamId, + resource, + userRole, + fallbackRoles = {}, +}: ResourcePermissionsOptions): Promise => { + const featureRepo = new FeaturesRepository(); + const permissionService = new PermissionCheckService(); + + const pbacEnabled = await featureRepo.checkIfTeamHasFeature(teamId, "pbac"); + + // If PBAC is disabled, use fallback role configuration + if (!pbacEnabled) { + return { + canRead: checkRoleAccess(userRole, fallbackRoles.read), + canCreate: checkRoleAccess(userRole, fallbackRoles.create), + canEdit: checkRoleAccess(userRole, fallbackRoles.update), + canDelete: checkRoleAccess(userRole, fallbackRoles.delete), + }; + } + + // PBAC is enabled, get permissions from the service + const resourcePermissions = await permissionService.getResourcePermissions({ + userId, + teamId, + resource, + }); + + const roleActions = PermissionMapper.toActionMap(resourcePermissions, resource); + + return { + canRead: roleActions[CrudAction.Read] ?? false, + canEdit: roleActions[CrudAction.Update] ?? false, + canDelete: roleActions[CrudAction.Delete] ?? false, + canCreate: roleActions[CrudAction.Create] ?? false, + }; +}; From cb5c51ea8c9a6d82feaadad907817d34b41a3edf Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Fri, 11 Jul 2025 15:40:08 +0100 Subject: [PATCH 05/25] org general page --- .../my-account/general/page.tsx | 35 ++++++++++++++- .../organizations/general/page.tsx | 31 ++++++++++++- ...DisablePhoneOnlySMSNotificationsSwitch.tsx | 5 +-- .../pages/components/LockEventTypeSwitch.tsx | 11 ++--- .../components/NoSlotsNotificationSwitch.tsx | 5 +-- .../organizations/pages/settings/general.tsx | 43 +++++++++++-------- 6 files changed, 94 insertions(+), 36 deletions(-) diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/general/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/general/page.tsx index 03cbc8ecb67201..0c5e5e561945ce 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/general/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/general/page.tsx @@ -1,8 +1,16 @@ import { _generateMetadata } from "app/_utils"; import { getTranslate } from "app/_utils"; import { revalidatePath } from "next/cache"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; import GeneralQueryView from "~/settings/my-account/general-view"; @@ -16,15 +24,38 @@ export const generateMetadata = async () => ); const Page = async () => { - const t = await getTranslate(); const revalidatePage = async () => { "use server"; revalidatePath("settings/my-account/general"); }; + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + const t = await getTranslate(); + + if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { + return redirect("/settings/profile"); + } + + const { canEdit } = await getResourcePermissions({ + userId: session.user.id, + teamId: session.user.profile.organizationId, + resource: Resource.Organization, + userRole: session.user.org.role, + fallbackRoles: { + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + return ( - + ); }; diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx index 75aeaf2f6e8b36..795e4bdacdc3d4 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/general/page.tsx @@ -1,7 +1,15 @@ import { _generateMetadata, getTranslate } from "app/_utils"; +import { headers, cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import LegacyPage from "@calcom/features/ee/organizations/pages/settings/general"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { MembershipRole } from "@calcom/prisma/enums"; + +import { buildLegacyRequest } from "@lib/buildLegacyCtx"; export const generateMetadata = async () => await _generateMetadata( @@ -15,9 +23,30 @@ export const generateMetadata = async () => const Page = async () => { const t = await getTranslate(); + const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); + + if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { + return redirect("/settings/profile"); + } + + const { canRead, canEdit, canDelete } = await getResourcePermissions({ + userId: session.user.id, + teamId: session.user.profile.organizationId, + resource: Resource.Organization, + userRole: session.user.org.role, + fallbackRoles: { + read: { + roles: [MembershipRole.MEMBER], + }, + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + return ( - + ); }; diff --git a/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx b/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx index 9e499828e8debd..b1852f86857098 100644 --- a/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx +++ b/packages/features/ee/organizations/pages/components/DisablePhoneOnlySMSNotificationsSwitch.tsx @@ -10,10 +10,9 @@ import { showToast } from "@calcom/ui/components/toast"; interface GeneralViewProps { currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"]; - isAdminOrOwner: boolean; } -export const DisablePhoneOnlySMSNotificationsSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewProps) => { +export const DisablePhoneOnlySMSNotificationsSwitch = ({ currentOrg }: GeneralViewProps) => { const { t } = useLocale(); const utils = trpc.useUtils(); const [disablePhoneOnlySMSNotificationsActive, setDisablePhoneOnlySMSNotificationsActive] = useState( @@ -32,8 +31,6 @@ export const DisablePhoneOnlySMSNotificationsSwitch = ({ currentOrg, isAdminOrOw }, }); - if (!isAdminOrOwner) return null; - return ( <> { +export const LockEventTypeSwitch = ({ currentOrg }: GeneralViewProps) => { const [lockEventTypeCreationForUsers, setLockEventTypeCreationForUsers] = useState( !!currentOrg.organizationSettings.lockEventTypeCreationForUsers ); @@ -50,8 +49,6 @@ export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewP }, }); - if (!isAdminOrOwner) return null; - const currentLockedOption = formMethods.watch("currentEventTypeOptions"); const { reset, getValues } = formMethods; @@ -69,7 +66,7 @@ export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewP { @@ -124,9 +121,7 @@ export const LockEventTypeSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewP - + diff --git a/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx b/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx index 4494251490179d..4d714409df8416 100644 --- a/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx +++ b/packages/features/ee/organizations/pages/components/NoSlotsNotificationSwitch.tsx @@ -8,10 +8,9 @@ import { showToast } from "@calcom/ui/components/toast"; interface GeneralViewProps { currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"]; - isAdminOrOwner: boolean; } -export const NoSlotsNotificationSwitch = ({ currentOrg, isAdminOrOwner }: GeneralViewProps) => { +export const NoSlotsNotificationSwitch = ({ currentOrg }: GeneralViewProps) => { const { t } = useLocale(); const utils = trpc.useUtils(); const [notificationActive, setNotificationActive] = useState( @@ -30,8 +29,6 @@ export const NoSlotsNotificationSwitch = ({ currentOrg, isAdminOrOwner }: Genera }, }); - if (!isAdminOrOwner) return null; - return ( <> { interface GeneralViewProps { currentOrg: RouterOutputs["viewer"]["organizations"]["listCurrent"]; - isAdminOrOwner: boolean; localeProp: string; + + permissions: { + canRead: boolean; + canEdit: boolean; + }; } -const OrgGeneralView = () => { +const OrgGeneralView = ({ + permissions, +}: { + permissions: { + canRead: boolean; + canEdit: boolean; + }; +}) => { const { t } = useLocale(); const router = useRouter(); const session = useSession(); - const isAdminOrOwner = checkAdminOrOwner(session.data?.user?.org?.role); const { data: currentOrg, @@ -75,20 +84,20 @@ const OrgGeneralView = () => { return ( - - - - - + + + {permissions.canEdit && ( + <> + + + + + )} ); }; -const GeneralView = ({ currentOrg, isAdminOrOwner, localeProp }: GeneralViewProps) => { +const GeneralView = ({ currentOrg, permissions, localeProp }: GeneralViewProps) => { const { t } = useLocale(); const mutation = trpc.viewer.organizations.update.useMutation({ @@ -136,7 +145,7 @@ const GeneralView = ({ currentOrg, isAdminOrOwner, localeProp }: GeneralViewProp reset, getValues, } = formMethods; - const isDisabled = isSubmitting || !isDirty || !isAdminOrOwner; + const isDisabled = isSubmitting || !isDirty || !permissions.canEdit; return (
- {isAdminOrOwner && ( + {permissions?.canEdit && ( + {permissions.canCreate && ( + + )} ) : (
@@ -173,13 +188,15 @@ function OrganizationAttributesPage() {

{t("add_attributes_description")}

- + {permissions.canCreate && ( + + )}
)} From f638bddb0aaf61762d7578a34291c41801dae344 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Mon, 14 Jul 2025 10:23:58 +0100 Subject: [PATCH 12/25] restore invite members --- .../my-account/general/page.tsx | 35 ++---------------- .../inviteMember/inviteMember.handler.ts | 19 ++++++---- .../viewer/teams/inviteMember/utils.ts | 36 ++++--------------- 3 files changed, 20 insertions(+), 70 deletions(-) diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/general/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/general/page.tsx index 0c5e5e561945ce..03cbc8ecb67201 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/general/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/my-account/general/page.tsx @@ -1,16 +1,8 @@ import { _generateMetadata } from "app/_utils"; import { getTranslate } from "app/_utils"; import { revalidatePath } from "next/cache"; -import { headers, cookies } from "next/headers"; -import { redirect } from "next/navigation"; -import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; -import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; -import { MembershipRole } from "@calcom/prisma/enums"; - -import { buildLegacyRequest } from "@lib/buildLegacyCtx"; import GeneralQueryView from "~/settings/my-account/general-view"; @@ -24,38 +16,15 @@ export const generateMetadata = async () => ); const Page = async () => { + const t = await getTranslate(); const revalidatePage = async () => { "use server"; revalidatePath("settings/my-account/general"); }; - const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); - const t = await getTranslate(); - - if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { - return redirect("/settings/profile"); - } - - const { canEdit } = await getResourcePermissions({ - userId: session.user.id, - teamId: session.user.profile.organizationId, - resource: Resource.Organization, - userRole: session.user.org.role, - fallbackRoles: { - update: { - roles: [MembershipRole.ADMIN, MembershipRole.OWNER], - }, - }, - }); - return ( - + ); }; diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts index c9759a66ebbca4..f210d63ae46c33 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler.ts @@ -5,6 +5,7 @@ import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowE import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { getTranslation } from "@calcom/lib/server/i18n"; +import { isOrganisationOwner } from "@calcom/lib/server/queries/organisations"; import { UserRepository } from "@calcom/lib/server/repository/user"; import prisma from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/enums"; @@ -17,7 +18,7 @@ import type { TInviteMemberInputSchema } from "./inviteMember.schema"; import type { TeamWithParent } from "./types"; import type { Invitation } from "./utils"; import { - ensureUserHasPermissions, + ensureAtleastAdminPermissions, findUsersWithInviteStatus, getOrgConnectionInfo, getOrgState, @@ -259,9 +260,9 @@ const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => { }); const isAddingNewOwner = !!invitations.find((invitation) => invitation.role === MembershipRole.OWNER); - // if (isTeamAnOrg) { - // await throwIfInviterCantAddOwnerToOrg(); - // } + if (isTeamAnOrg) { + await throwIfInviterCantAddOwnerToOrg(); + } if (isPlatform) { inviterOrgId = team.id; @@ -272,12 +273,11 @@ const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => { }); } - await ensureUserHasPermissions({ + await ensureAtleastAdminPermissions({ userId: inviter.id, - teamId: team.id, + teamId: inviterOrgId && isInviterOrgAdmin ? inviterOrgId : input.teamId, isOrg: isTeamAnOrg, }); - const result = await inviteMembersWithNoInviterPermissionCheck({ inviterName: inviter.name, team, @@ -287,6 +287,11 @@ const inviteMembers = async ({ ctx, input }: InviteMemberOptions) => { invitations, }); return result; + + async function throwIfInviterCantAddOwnerToOrg() { + const isInviterOrgOwner = await isOrganisationOwner(inviter.id, input.teamId); + if (isAddingNewOwner && !isInviterOrgOwner) throw new TRPCError({ code: "UNAUTHORIZED" }); + } }; export default async function inviteMemberHandler({ ctx, input }: InviteMemberOptions) { diff --git a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts index 9977f49a6e86ea..14d44477ca0b8c 100644 --- a/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts +++ b/packages/trpc/server/routers/viewer/teams/inviteMember/utils.ts @@ -4,11 +4,6 @@ import type { TFunction } from "i18next"; import { getOrgFullOrigin } from "@calcom/ee/organizations/lib/orgDomains"; import { sendTeamInviteEmail } from "@calcom/emails"; import { checkAdminOrOwner } from "@calcom/features/auth/lib/checkAdminOrOwner"; -import { - RoleManagementError, - RoleManagementErrorCode, -} from "@calcom/features/pbac/domain/errors/role-management.error"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability"; import { ENABLE_PROFILE_SWITCHER, WEBAPP_URL } from "@calcom/lib/constants"; import { createAProfileForAnExistingUser } from "@calcom/lib/createAProfileForAnExistingUser"; @@ -62,31 +57,6 @@ type InvitableExistingUserWithProfile = InvitableExistingUser & { } | null; }; -export async function ensureUserHasPermissions({ - userId, - teamId, - isOrg, -}: { - userId: number; - teamId: number; - isOrg?: boolean; -}) { - const permissionCheckService = new PermissionCheckService(); - - const hasPermission = await permissionCheckService.checkPermission({ - userId, - teamId, - permission: isOrg ? "organization.invite" : "team.invite", - fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], - }); - - if (!hasPermission) - throw new RoleManagementError( - "You do not have permission to invite members.", - RoleManagementErrorCode.UNAUTHORIZED - ); -} - export async function ensureAtleastAdminPermissions({ userId, teamId, @@ -755,6 +725,12 @@ export const sendExistingUserTeamInviteEmails = async ({ await sendEmails(sendEmailsPromises); }; +type inviteMemberHandlerInput = { + teamId: number; + role?: "ADMIN" | "MEMBER" | "OWNER"; + language: string; +}; + export async function handleExistingUsersInvites({ invitableExistingUsers, team, From 3185e4448b5be73fba0e3e05795f09205f10bccf Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Mon, 14 Jul 2025 10:24:29 +0100 Subject: [PATCH 13/25] update org update and attribute backends --- .../viewer/attributes/create.handler.ts | 26 +++++++++++++++++-- .../viewer/attributes/delete.handler.ts | 25 +++++++++++++++++- .../routers/viewer/attributes/edit.handler.ts | 25 +++++++++++++++++- .../viewer/attributes/toggleActive.handler.ts | 25 +++++++++++++++++- .../viewer/organizations/update.handler.ts | 21 ++++++++++----- 5 files changed, 110 insertions(+), 12 deletions(-) diff --git a/packages/trpc/server/routers/viewer/attributes/create.handler.ts b/packages/trpc/server/routers/viewer/attributes/create.handler.ts index 5db37a6c2ac930..fdf6769e49fdee 100644 --- a/packages/trpc/server/routers/viewer/attributes/create.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/create.handler.ts @@ -1,7 +1,9 @@ +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; import type { Attribute } from "@calcom/prisma/client"; -import { Prisma } from "@calcom/prisma/client"; +import { Prisma, MembershipRole } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; @@ -20,14 +22,34 @@ const typesWithOptions = ["SINGLE_SELECT", "MULTI_SELECT"]; const createAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; + const userRole = ctx.user.org?.role; - if (!org.id) { + if (!org.id || !userRole) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", }); } + const { canCreate } = await getResourcePermissions({ + userId: ctx.user.id, + teamId: org.id, + resource: Resource.Attributes, + userRole, + fallbackRoles: { + create: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + + if (!canCreate) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have permission to create attributes", + }); + } + const slug = slugify(input.name); const uniqueOptions = getOptionsWithValidContains(input.options); diff --git a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts index 7f533981e9d0d2..b539c796cdfcd0 100644 --- a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts @@ -1,4 +1,7 @@ +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import prisma from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; @@ -14,14 +17,34 @@ type DeleteOptions = { const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { const org = ctx.user.organization; + const userRole = ctx.user.org?.role; - if (!org.id) { + if (!org.id || !userRole) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", }); } + const { canDelete } = await getResourcePermissions({ + userId: ctx.user.id, + teamId: org.id, + resource: Resource.Attributes, + userRole, + fallbackRoles: { + delete: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + + if (!canDelete) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have permission to delete attributes", + }); + } + const attribute = await prisma.attribute.delete({ where: { teamId: org.id, diff --git a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts index b37c685866ceca..c0050660b2ae85 100644 --- a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts @@ -1,5 +1,8 @@ +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; @@ -16,14 +19,34 @@ type GetOptions = { const editAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; + const userRole = ctx.user.org?.role; - if (!org.id) { + if (!org.id || !userRole) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", }); } + const { canEdit } = await getResourcePermissions({ + userId: ctx.user.id, + teamId: org.id, + resource: Resource.Attributes, + userRole, + fallbackRoles: { + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + + if (!canEdit) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have permission to edit attributes", + }); + } + // If an option is removed, it is to be removed from contains of corresponding group as well if any const options = getOptionsWithValidContains(input.options); diff --git a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts index 5ea10e3c47d310..fb5124f5794443 100644 --- a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts @@ -1,4 +1,7 @@ +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import prisma from "@calcom/prisma"; +import { MembershipRole } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; @@ -14,14 +17,34 @@ type GetOptions = { const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; + const userRole = ctx.user.org?.role; - if (!org.id) { + if (!org.id || !userRole) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", }); } + const { canEdit } = await getResourcePermissions({ + userId: ctx.user.id, + teamId: org.id, + resource: Resource.Attributes, + userRole, + fallbackRoles: { + update: { + roles: [MembershipRole.ADMIN, MembershipRole.OWNER], + }, + }, + }); + + if (!canEdit) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You don't have permission to modify attributes", + }); + } + // Ensure that this users org owns the attribute const attribute = await prisma.attribute.findUnique({ where: { diff --git a/packages/trpc/server/routers/viewer/organizations/update.handler.ts b/packages/trpc/server/routers/viewer/organizations/update.handler.ts index f394c95c712460..5bb75d64bff094 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.handler.ts @@ -1,6 +1,7 @@ import type { Prisma } from "@prisma/client"; -import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service"; +import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; +import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants"; import { getMetadataHelpers } from "@calcom/lib/getMetadataHelpers"; import { uploadLogo } from "@calcom/lib/server/avatar"; @@ -111,16 +112,22 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { if (!currentOrgId) throw new TRPCError({ code: "BAD_REQUEST", message: "Organization ID is required." }); - const permissionCheckService = new PermissionCheckService(); + const userRole = ctx.user?.org?.role; + if (!userRole) throw new TRPCError({ code: "UNAUTHORIZED", message: "User role is required." }); - const isUserAuthorizedToUpdate = await permissionCheckService.checkPermission({ - userId: ctx.user?.id, + const { canEdit } = await getResourcePermissions({ + userId: ctx.user.id, teamId: currentOrgId, - permission: "organization.update", - fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN], + resource: Resource.Organization, + userRole, + fallbackRoles: { + update: { + roles: [MembershipRole.OWNER, MembershipRole.ADMIN], + }, + }, }); - if (!isUserAuthorizedToUpdate) throw new TRPCError({ code: "UNAUTHORIZED" }); + if (!canEdit) throw new TRPCError({ code: "UNAUTHORIZED" }); if (input.slug) { const userConflict = await prisma.team.findMany({ From 799743eda9201edf7ed63d7c8694ac996f925ab2 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Tue, 15 Jul 2025 10:50:35 +0100 Subject: [PATCH 14/25] fix type errors --- .../routers/viewer/attributes/create.handler.ts | 15 ++++++++++++--- .../routers/viewer/attributes/delete.handler.ts | 15 ++++++++++++--- .../routers/viewer/attributes/edit.handler.ts | 15 ++++++++++++--- .../viewer/attributes/toggleActive.handler.ts | 14 +++++++++++--- .../viewer/organizations/update.handler.ts | 15 ++++++++++++--- 5 files changed, 59 insertions(+), 15 deletions(-) diff --git a/packages/trpc/server/routers/viewer/attributes/create.handler.ts b/packages/trpc/server/routers/viewer/attributes/create.handler.ts index fdf6769e49fdee..d85984aebdc9b5 100644 --- a/packages/trpc/server/routers/viewer/attributes/create.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/create.handler.ts @@ -22,9 +22,18 @@ const typesWithOptions = ["SINGLE_SELECT", "MULTI_SELECT"]; const createAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; - const userRole = ctx.user.org?.role; - if (!org.id || !userRole) { + const { role: userOrgRole } = await prisma.membership.findFirst({ + where: { + userId: ctx.user.id, + organizationId: org.id, + }, + select: { + role: true, + }, + }); + + if (!org.id || !userOrgRole) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -35,7 +44,7 @@ const createAttributesHandler = async ({ input, ctx }: GetOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole, + userRole: userOrgRole, fallbackRoles: { create: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts index b539c796cdfcd0..48df8d8ac5fa15 100644 --- a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts @@ -17,9 +17,18 @@ type DeleteOptions = { const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { const org = ctx.user.organization; - const userRole = ctx.user.org?.role; - if (!org.id || !userRole) { + const { role: userOrgRole } = await prisma.membership.findFirst({ + where: { + userId: ctx.user.id, + organizationId: org.id, + }, + select: { + role: true, + }, + }); + + if (!org.id || !userOrgRole) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -30,7 +39,7 @@ const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole, + userRole: userOrgRole, fallbackRoles: { delete: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts index c0050660b2ae85..549021a5819106 100644 --- a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts @@ -19,9 +19,18 @@ type GetOptions = { const editAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; - const userRole = ctx.user.org?.role; - if (!org.id || !userRole) { + const { role: userOrgRole } = await prisma.membership.findFirst({ + where: { + userId: ctx.user.id, + organizationId: org.id, + }, + select: { + role: true, + }, + }); + + if (!org.id || !userOrgRole) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -32,7 +41,7 @@ const editAttributesHandler = async ({ input, ctx }: GetOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole, + userRole: userOrgRole, fallbackRoles: { update: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts index fb5124f5794443..13df4335e3175a 100644 --- a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts @@ -17,9 +17,17 @@ type GetOptions = { const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; - const userRole = ctx.user.org?.role; + const { role: userOrgRole } = await prisma.membership.findFirst({ + where: { + userId: ctx.user.id, + organizationId: org.id, + }, + select: { + role: true, + }, + }); - if (!org.id || !userRole) { + if (!org.id || !userOrgRole) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -30,7 +38,7 @@ const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole, + userRole: userOrgRole, fallbackRoles: { update: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/organizations/update.handler.ts b/packages/trpc/server/routers/viewer/organizations/update.handler.ts index 5bb75d64bff094..6152d5d6f23d64 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.handler.ts @@ -112,14 +112,23 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { if (!currentOrgId) throw new TRPCError({ code: "BAD_REQUEST", message: "Organization ID is required." }); - const userRole = ctx.user?.org?.role; - if (!userRole) throw new TRPCError({ code: "UNAUTHORIZED", message: "User role is required." }); + const { role: userOrgRole } = await prisma.membership.findFirst({ + where: { + userId: ctx.user.id, + organizationId: org.id, + }, + select: { + role: true, + }, + }); + + if (!userOrgRole) throw new TRPCError({ code: "UNAUTHORIZED", message: "User role is required." }); const { canEdit } = await getResourcePermissions({ userId: ctx.user.id, teamId: currentOrgId, resource: Resource.Organization, - userRole, + userRole: userOrgRole, fallbackRoles: { update: { roles: [MembershipRole.OWNER, MembershipRole.ADMIN], From 75937580a147ca83e4407c6d05ee4295d230b3c4 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Tue, 15 Jul 2025 11:13:33 +0100 Subject: [PATCH 15/25] fix orgId --- .../trpc/server/routers/viewer/organizations/update.handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/trpc/server/routers/viewer/organizations/update.handler.ts b/packages/trpc/server/routers/viewer/organizations/update.handler.ts index 6152d5d6f23d64..96e3bf4b8b52b3 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.handler.ts @@ -115,7 +115,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { const { role: userOrgRole } = await prisma.membership.findFirst({ where: { userId: ctx.user.id, - organizationId: org.id, + organizationId: currentOrgId, }, select: { role: true, From d84119b58d247418540bf00cab8f8054fc067048 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Tue, 15 Jul 2025 11:25:46 +0100 Subject: [PATCH 16/25] fix types attempt two --- .../trpc/server/routers/viewer/attributes/create.handler.ts | 6 +++--- .../trpc/server/routers/viewer/attributes/delete.handler.ts | 6 +++--- .../trpc/server/routers/viewer/attributes/edit.handler.ts | 6 +++--- .../routers/viewer/attributes/toggleActive.handler.ts | 6 +++--- .../server/routers/viewer/organizations/update.handler.ts | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/trpc/server/routers/viewer/attributes/create.handler.ts b/packages/trpc/server/routers/viewer/attributes/create.handler.ts index d85984aebdc9b5..824591f8d47abe 100644 --- a/packages/trpc/server/routers/viewer/attributes/create.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/create.handler.ts @@ -23,7 +23,7 @@ const typesWithOptions = ["SINGLE_SELECT", "MULTI_SELECT"]; const createAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, organizationId: org.id, @@ -33,7 +33,7 @@ const createAttributesHandler = async ({ input, ctx }: GetOptions) => { }, }); - if (!org.id || !userOrgRole) { + if (!org.id || !membership.role) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -44,7 +44,7 @@ const createAttributesHandler = async ({ input, ctx }: GetOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole: userOrgRole, + userRole: membership.role, fallbackRoles: { create: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts index 48df8d8ac5fa15..55e52652b890c8 100644 --- a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts @@ -18,7 +18,7 @@ type DeleteOptions = { const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { const org = ctx.user.organization; - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, organizationId: org.id, @@ -28,7 +28,7 @@ const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { }, }); - if (!org.id || !userOrgRole) { + if (!org.id || !membership) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -39,7 +39,7 @@ const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole: userOrgRole, + userRole: membership.role, fallbackRoles: { delete: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts index 549021a5819106..679d232d5ff140 100644 --- a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts @@ -20,7 +20,7 @@ type GetOptions = { const editAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, organizationId: org.id, @@ -30,7 +30,7 @@ const editAttributesHandler = async ({ input, ctx }: GetOptions) => { }, }); - if (!org.id || !userOrgRole) { + if (!org.id || !membership) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -41,7 +41,7 @@ const editAttributesHandler = async ({ input, ctx }: GetOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole: userOrgRole, + userRole: membership.role, fallbackRoles: { update: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts index 13df4335e3175a..bd81a68e0b941e 100644 --- a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts @@ -17,7 +17,7 @@ type GetOptions = { const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, organizationId: org.id, @@ -27,7 +27,7 @@ const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { }, }); - if (!org.id || !userOrgRole) { + if (!org.id || !membership) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -38,7 +38,7 @@ const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole: userOrgRole, + userRole: membership.role, fallbackRoles: { update: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/organizations/update.handler.ts b/packages/trpc/server/routers/viewer/organizations/update.handler.ts index 96e3bf4b8b52b3..489b3bb1398a12 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.handler.ts @@ -112,7 +112,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { if (!currentOrgId) throw new TRPCError({ code: "BAD_REQUEST", message: "Organization ID is required." }); - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, organizationId: currentOrgId, @@ -122,13 +122,13 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { }, }); - if (!userOrgRole) throw new TRPCError({ code: "UNAUTHORIZED", message: "User role is required." }); + if (!membership) throw new TRPCError({ code: "UNAUTHORIZED", message: "User role is required." }); const { canEdit } = await getResourcePermissions({ userId: ctx.user.id, teamId: currentOrgId, resource: Resource.Organization, - userRole: userOrgRole, + userRole: membership.role, fallbackRoles: { update: { roles: [MembershipRole.OWNER, MembershipRole.ADMIN], From bbd4901b7168d47ab2903e1979fec740bd73b393 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Tue, 15 Jul 2025 11:25:46 +0100 Subject: [PATCH 17/25] fix types attempt two --- .../server/routers/viewer/attributes/create.handler.ts | 8 ++++---- .../server/routers/viewer/attributes/delete.handler.ts | 8 ++++---- .../trpc/server/routers/viewer/attributes/edit.handler.ts | 8 ++++---- .../routers/viewer/attributes/toggleActive.handler.ts | 8 ++++---- .../server/routers/viewer/organizations/update.handler.ts | 8 ++++---- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/trpc/server/routers/viewer/attributes/create.handler.ts b/packages/trpc/server/routers/viewer/attributes/create.handler.ts index d85984aebdc9b5..2fe896195b8b3e 100644 --- a/packages/trpc/server/routers/viewer/attributes/create.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/create.handler.ts @@ -23,17 +23,17 @@ const typesWithOptions = ["SINGLE_SELECT", "MULTI_SELECT"]; const createAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, - organizationId: org.id, + teamId: org.id, }, select: { role: true, }, }); - if (!org.id || !userOrgRole) { + if (!org.id || !membership?.role) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -44,7 +44,7 @@ const createAttributesHandler = async ({ input, ctx }: GetOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole: userOrgRole, + userRole: membership!.role, fallbackRoles: { create: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts index 48df8d8ac5fa15..06bab67ad4dbb8 100644 --- a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts @@ -18,17 +18,17 @@ type DeleteOptions = { const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { const org = ctx.user.organization; - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, - organizationId: org.id, + teamId: org.id, }, select: { role: true, }, }); - if (!org.id || !userOrgRole) { + if (!org.id || !membership) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -39,7 +39,7 @@ const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole: userOrgRole, + userRole: membership.role, fallbackRoles: { delete: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts index 549021a5819106..618023fae0f4d3 100644 --- a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts @@ -20,17 +20,17 @@ type GetOptions = { const editAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, - organizationId: org.id, + teamId: org.id, }, select: { role: true, }, }); - if (!org.id || !userOrgRole) { + if (!org.id || !membership) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -41,7 +41,7 @@ const editAttributesHandler = async ({ input, ctx }: GetOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole: userOrgRole, + userRole: membership.role, fallbackRoles: { update: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts index 13df4335e3175a..76f79fa6cdf893 100644 --- a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts @@ -17,17 +17,17 @@ type GetOptions = { const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, - organizationId: org.id, + teamId: org.id, }, select: { role: true, }, }); - if (!org.id || !userOrgRole) { + if (!org.id || !membership) { throw new TRPCError({ code: "UNAUTHORIZED", message: "You need to be apart of an organization to use this feature", @@ -38,7 +38,7 @@ const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { userId: ctx.user.id, teamId: org.id, resource: Resource.Attributes, - userRole: userOrgRole, + userRole: membership.role, fallbackRoles: { update: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], diff --git a/packages/trpc/server/routers/viewer/organizations/update.handler.ts b/packages/trpc/server/routers/viewer/organizations/update.handler.ts index 96e3bf4b8b52b3..e112f055a160b1 100644 --- a/packages/trpc/server/routers/viewer/organizations/update.handler.ts +++ b/packages/trpc/server/routers/viewer/organizations/update.handler.ts @@ -112,23 +112,23 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { if (!currentOrgId) throw new TRPCError({ code: "BAD_REQUEST", message: "Organization ID is required." }); - const { role: userOrgRole } = await prisma.membership.findFirst({ + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, - organizationId: currentOrgId, + teamId: currentOrgId, }, select: { role: true, }, }); - if (!userOrgRole) throw new TRPCError({ code: "UNAUTHORIZED", message: "User role is required." }); + if (!membership) throw new TRPCError({ code: "UNAUTHORIZED", message: "User role is required." }); const { canEdit } = await getResourcePermissions({ userId: ctx.user.id, teamId: currentOrgId, resource: Resource.Organization, - userRole: userOrgRole, + userRole: membership.role, fallbackRoles: { update: { roles: [MembershipRole.OWNER, MembershipRole.ADMIN], From ec7c5a497f94482afb0941b30b8d66c672f89dfd Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Wed, 16 Jul 2025 11:30:19 +0100 Subject: [PATCH 18/25] fix type error --- .../routers/viewer/attributes/create.handler.ts | 11 +++++++++-- .../routers/viewer/attributes/delete.handler.ts | 11 +++++++++-- .../server/routers/viewer/attributes/edit.handler.ts | 11 +++++++++-- .../viewer/attributes/toggleActive.handler.ts | 12 ++++++++++-- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/packages/trpc/server/routers/viewer/attributes/create.handler.ts b/packages/trpc/server/routers/viewer/attributes/create.handler.ts index 42338d74756d93..4f75ac1dfb68e8 100644 --- a/packages/trpc/server/routers/viewer/attributes/create.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/create.handler.ts @@ -23,6 +23,13 @@ const typesWithOptions = ["SINGLE_SELECT", "MULTI_SELECT"]; const createAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; + if (!org.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to be apart of an organization to use this feature", + }); + } + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, @@ -33,10 +40,10 @@ const createAttributesHandler = async ({ input, ctx }: GetOptions) => { }, }); - if (!org.id || !membership?.role) { + if (!membership) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "You need to be apart of an organization to use this feature", + message: "You need to be apart of this organization to use this feature", }); } diff --git a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts index 06bab67ad4dbb8..714e18f6b50c29 100644 --- a/packages/trpc/server/routers/viewer/attributes/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/delete.handler.ts @@ -18,6 +18,13 @@ type DeleteOptions = { const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { const org = ctx.user.organization; + if (!org.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to be apart of an organization to use this feature", + }); + } + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, @@ -28,10 +35,10 @@ const deleteAttributeHandler = async ({ input, ctx }: DeleteOptions) => { }, }); - if (!org.id || !membership) { + if (!membership) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "You need to be apart of an organization to use this feature", + message: "You need to be apart of this organization to use this feature", }); } diff --git a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts index 618023fae0f4d3..4be352cee0cdf4 100644 --- a/packages/trpc/server/routers/viewer/attributes/edit.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/edit.handler.ts @@ -20,6 +20,13 @@ type GetOptions = { const editAttributesHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; + if (!org.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to be apart of an organization to use this feature", + }); + } + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, @@ -30,10 +37,10 @@ const editAttributesHandler = async ({ input, ctx }: GetOptions) => { }, }); - if (!org.id || !membership) { + if (!membership) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "You need to be apart of an organization to use this feature", + message: "You need to be apart of this organization to use this feature", }); } diff --git a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts index 76f79fa6cdf893..e9b19522f3ce89 100644 --- a/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts +++ b/packages/trpc/server/routers/viewer/attributes/toggleActive.handler.ts @@ -17,6 +17,14 @@ type GetOptions = { const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { const org = ctx.user.organization; + + if (!org.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "You need to be apart of an organization to use this feature", + }); + } + const membership = await prisma.membership.findFirst({ where: { userId: ctx.user.id, @@ -27,10 +35,10 @@ const toggleActiveHandler = async ({ input, ctx }: GetOptions) => { }, }); - if (!org.id || !membership) { + if (!membership) { throw new TRPCError({ code: "UNAUTHORIZED", - message: "You need to be apart of an organization to use this feature", + message: "You need to be apart of this organization to use this feature", }); } From 28b789f08a585d64a7686bf0e247c9757fc6ed58 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Mon, 28 Jul 2025 14:05:02 +0100 Subject: [PATCH 19/25] fix dupe string in i18n --- apps/web/public/static/locales/en/common.json | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index ea6da8c0ee393e..fe8f9ffdd56610 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3308,7 +3308,6 @@ "error_creating_role": "Error creating role", "error_updating_role": "Error updating role", "pbac_desc_create_roles": "Create roles", - "pbac_desc_manage_roles": "Manage roles", "pbac_resource_attributes": "Attributes", "pbac_desc_view_organization_attributes": "View organization attributes", "pbac_desc_update_organization_attributes": "Update organization attributes", From 9f80c57d8f5b2abc1545fcdbac1c0cb666165520 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Wed, 30 Jul 2025 09:10:03 +0100 Subject: [PATCH 20/25] fix tests --- .../settings/(settings-layout)/organizations/profile/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx index fcbe2c42507cab..0962d33607e044 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx @@ -24,13 +24,13 @@ const Page = async () => { const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); const t = await getTranslate(); - if (!session?.user.id || !session?.user.profile?.organizationId || !session?.user.org) { + if (!session?.user.id || !session?.user.org) { return redirect("/settings/profile"); } const { canRead, canEdit, canDelete } = await getResourcePermissions({ userId: session.user.id, - teamId: session.user.profile.organizationId, + teamId: session?.user.org.id, resource: Resource.Organization, userRole: session.user.org.role, fallbackRoles: { From 667ed6f511a69b7b644a7e6e4bf4200e59966d5e Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Wed, 30 Jul 2025 10:13:53 +0100 Subject: [PATCH 21/25] fix team-dsync --- .../features/ee/dsync/page/team-dsync-view.tsx | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/features/ee/dsync/page/team-dsync-view.tsx b/packages/features/ee/dsync/page/team-dsync-view.tsx index 5f4f2d84463e93..c8a453ba2e1a8c 100644 --- a/packages/features/ee/dsync/page/team-dsync-view.tsx +++ b/packages/features/ee/dsync/page/team-dsync-view.tsx @@ -11,14 +11,8 @@ import { showToast } from "@calcom/ui/components/toast"; import ConfigureDirectorySync from "../components/ConfigureDirectorySync"; -interface DirectorySyncTeamViewProps { - permissions?: { - canEdit: boolean; - }; -} - // For Hosted Cal - Team view -const DirectorySync = ({ permissions }: DirectorySyncTeamViewProps) => { +const DirectorySync = () => { const { t } = useLocale(); const router = useRouter(); @@ -42,16 +36,6 @@ const DirectorySync = ({ permissions }: DirectorySyncTeamViewProps) => { showToast(error.message, "error"); } - const canEdit = permissions?.canEdit ?? false; - - if (!canEdit) { - return ( -
- {t("only_admin_can_manage_directory_sync")} -
- ); - } - return (
{HOSTED_CAL_FEATURES && } From 194cc30d084bf6ce035e2ed0b3649f95b425033e Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Wed, 30 Jul 2025 10:45:14 +0100 Subject: [PATCH 22/25] update session to use profile --- .../settings/(settings-layout)/organizations/profile/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx index 0962d33607e044..a442437243d8d0 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx @@ -24,13 +24,13 @@ const Page = async () => { const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); const t = await getTranslate(); - if (!session?.user.id || !session?.user.org) { + if (!session?.user.id || !session?.user.profile?.organizationId) { return redirect("/settings/profile"); } const { canRead, canEdit, canDelete } = await getResourcePermissions({ userId: session.user.id, - teamId: session?.user.org.id, + teamId: session?.user.profile?.organizationId, resource: Resource.Organization, userRole: session.user.org.role, fallbackRoles: { From 51c0a47a9420a1c9ac9679eda210cda8763093b0 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Wed, 30 Jul 2025 10:53:32 +0100 Subject: [PATCH 23/25] use profile metadata to get orgRolew --- .../(settings-layout)/organizations/profile/page.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx index a442437243d8d0..958c5fe9e813bd 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx @@ -24,7 +24,11 @@ const Page = async () => { const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) }); const t = await getTranslate(); - if (!session?.user.id || !session?.user.profile?.organizationId) { + const orgRole = session?.user.profile?.organization.members?.find( + (member) => member.userId === session?.user.id + )?.role; + + if (!session?.user.id || !session?.user.profile?.organizationId || !orgRole) { return redirect("/settings/profile"); } @@ -32,7 +36,7 @@ const Page = async () => { userId: session.user.id, teamId: session?.user.profile?.organizationId, resource: Resource.Organization, - userRole: session.user.org.role, + userRole: orgRole, fallbackRoles: { read: { roles: [MembershipRole.ADMIN, MembershipRole.OWNER], From 603f9a1fa45beee41a16c14bdcd0d02692b69bb7 Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Wed, 30 Jul 2025 11:03:59 +0100 Subject: [PATCH 24/25] fix dsync --- packages/features/ee/dsync/page/team-dsync-view.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/features/ee/dsync/page/team-dsync-view.tsx b/packages/features/ee/dsync/page/team-dsync-view.tsx index c8a453ba2e1a8c..bf8202f3af2e25 100644 --- a/packages/features/ee/dsync/page/team-dsync-view.tsx +++ b/packages/features/ee/dsync/page/team-dsync-view.tsx @@ -12,7 +12,7 @@ import { showToast } from "@calcom/ui/components/toast"; import ConfigureDirectorySync from "../components/ConfigureDirectorySync"; // For Hosted Cal - Team view -const DirectorySync = () => { +const DirectorySync = ({ permissions }: { permissions?: { canEdit: boolean } }) => { const { t } = useLocale(); const router = useRouter(); @@ -36,6 +36,10 @@ const DirectorySync = () => { showToast(error.message, "error"); } + if (!permissions?.canEdit) { + router.push("/404"); + } + return (
{HOSTED_CAL_FEATURES && } From 8e2df352b014175028a159d1235d6bc228b3914d Mon Sep 17 00:00:00 2001 From: Sean Brydon Date: Wed, 30 Jul 2025 11:06:21 +0100 Subject: [PATCH 25/25] fix typing --- .../settings/(settings-layout)/organizations/profile/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx index 958c5fe9e813bd..4b58650b45d306 100644 --- a/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx +++ b/apps/web/app/(use-page-wrapper)/settings/(settings-layout)/organizations/profile/page.tsx @@ -7,6 +7,7 @@ import LegacyPage from "@calcom/features/ee/organizations/pages/settings/profile import { Resource } from "@calcom/features/pbac/domain/types/permission-registry"; import { getResourcePermissions } from "@calcom/features/pbac/lib/resource-permissions"; import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import type { Membership } from "@calcom/prisma/client"; import { MembershipRole } from "@calcom/prisma/enums"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; @@ -25,7 +26,7 @@ const Page = async () => { const t = await getTranslate(); const orgRole = session?.user.profile?.organization.members?.find( - (member) => member.userId === session?.user.id + (member: Membership) => member.userId === session?.user.id )?.role; if (!session?.user.id || !session?.user.profile?.organizationId || !orgRole) {