fix: use FeatureOptInService for bookings-v3 check (#27183)
* fix: use FeatureOptInService for bookings-v3 check * refactor: simplify FeatureOptInService API and fix bookings page - Add resolveFeatureStates method that takes only userId and featureIds - Update listFeaturesForUser to take only userId parameter - Remove getUserOrgAndTeamIds from _router.ts (now internal to service) - Fix bookings page to use new resolveFeatureStates method - Parallelize getUserFeaturesStatus and resolveFeatureStates calls Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * check session?.user?.id first * remove comment --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
987e727464
commit
ddceabe5d4
@@ -1,18 +1,15 @@
|
||||
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { getFeatureOptInService } from "@calcom/features/di/containers/FeatureOptInService";
|
||||
import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
import type { PageProps } from "app/_types";
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
|
||||
import { validStatuses } from "~/bookings/lib/validStatuses";
|
||||
import BookingsList from "~/bookings/views/bookings-view";
|
||||
|
||||
@@ -37,29 +34,36 @@ const Page = async ({ params }: PageProps) => {
|
||||
const t = await getTranslate();
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
|
||||
let canReadOthersBookings = false;
|
||||
if (session?.user?.id) {
|
||||
const permissionService = new PermissionCheckService();
|
||||
const userId = session.user.id;
|
||||
|
||||
const teamIdsWithPermission = await permissionService.getTeamIdsWithPermission({
|
||||
userId,
|
||||
permission: "booking.read",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
// We check if teamIdsWithPermission.length > 0.
|
||||
// While this may not be entirely accurate, it's acceptable
|
||||
// because we perform a thorough validation on the server side for the actual filter values.
|
||||
// This variable is primarily for UI purposes.
|
||||
canReadOthersBookings = teamIdsWithPermission.length > 0;
|
||||
if (!session?.user?.id) {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const featureFlags = session?.user?.id
|
||||
? await featuresRepository.getUserFeaturesStatus(session.user.id, ["bookings-v3", "booking-audit"])
|
||||
: { "bookings-v3": false, "booking-audit": false };
|
||||
const userId = session.user.id;
|
||||
const permissionService = new PermissionCheckService();
|
||||
|
||||
const bookingsV3Enabled = featureFlags["bookings-v3"] ?? false;
|
||||
const teamIdsWithPermission = await permissionService.getTeamIdsWithPermission({
|
||||
userId,
|
||||
permission: "booking.read",
|
||||
fallbackRoles: [MembershipRole.OWNER, MembershipRole.ADMIN],
|
||||
});
|
||||
// We check if teamIdsWithPermission.length > 0.
|
||||
// While this may not be entirely accurate, it's acceptable
|
||||
// because we perform a thorough validation on the server side for the actual filter values.
|
||||
// This variable is primarily for UI purposes.
|
||||
const canReadOthersBookings = teamIdsWithPermission.length > 0;
|
||||
|
||||
const featuresRepository = getFeaturesRepository();
|
||||
const featureOptInService = getFeatureOptInService();
|
||||
|
||||
const [featureFlags, featureStates] = await Promise.all([
|
||||
featuresRepository.getUserFeaturesStatus(userId, ["booking-audit"]),
|
||||
featureOptInService.resolveFeatureStates({
|
||||
userId,
|
||||
featureIds: ["bookings-v3"],
|
||||
}),
|
||||
]);
|
||||
|
||||
const bookingsV3Enabled = featureStates["bookings-v3"]?.effectiveEnabled ?? false;
|
||||
const bookingAuditEnabled = featureFlags["booking-audit"] ?? false;
|
||||
|
||||
return (
|
||||
@@ -69,7 +73,7 @@ const Page = async ({ params }: PageProps) => {
|
||||
headerClassName="bookings-shell-heading">
|
||||
<BookingsList
|
||||
status={parsed.data.status}
|
||||
userId={session?.user?.id}
|
||||
userId={userId}
|
||||
permissions={{ canReadOthersBookings }}
|
||||
bookingsV3Enabled={bookingsV3Enabled}
|
||||
bookingAuditEnabled={bookingAuditEnabled}
|
||||
|
||||
@@ -227,23 +227,32 @@ export class FeatureOptInService implements IFeatureOptInService {
|
||||
};
|
||||
}
|
||||
|
||||
async resolveFeatureStates(input: {
|
||||
userId: number;
|
||||
featureIds: FeatureId[];
|
||||
}): Promise<Record<string, ResolvedFeatureState>> {
|
||||
const { userId, featureIds } = input;
|
||||
const { orgId, teamIds } = await this.getUserOrgAndTeamIds(userId);
|
||||
|
||||
return this.resolveFeatureStatesAcrossTeams({
|
||||
userId,
|
||||
orgId,
|
||||
teamIds,
|
||||
featureIds,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List all opt-in features with their states for a user across teams.
|
||||
* Only returns features that are in the allowlist, globally enabled, and scoped to "user".
|
||||
*/
|
||||
async listFeaturesForUser(input: {
|
||||
userId: number;
|
||||
orgId: number | null;
|
||||
teamIds: number[];
|
||||
}): Promise<ListFeaturesForUserResult[]> {
|
||||
const { userId, orgId, teamIds } = input;
|
||||
async listFeaturesForUser(input: { userId: number }): Promise<ListFeaturesForUserResult[]> {
|
||||
const { userId } = input;
|
||||
const userScopedFeatures = getOptInFeaturesForScope("user");
|
||||
const featureIds = userScopedFeatures.map((config) => config.slug);
|
||||
|
||||
const resolvedStates = await this.resolveFeatureStatesAcrossTeams({
|
||||
const resolvedStates = await this.resolveFeatureStates({
|
||||
userId,
|
||||
orgId,
|
||||
teamIds,
|
||||
featureIds,
|
||||
});
|
||||
|
||||
|
||||
@@ -64,11 +64,11 @@ export interface IFeatureOptInService {
|
||||
teamIds: number[];
|
||||
featureIds: FeatureId[];
|
||||
}): Promise<Record<string, ResolvedFeatureState>>;
|
||||
listFeaturesForUser(input: {
|
||||
resolveFeatureStates(input: {
|
||||
userId: number;
|
||||
orgId: number | null;
|
||||
teamIds: number[];
|
||||
}): Promise<ResolvedFeatureState[]>;
|
||||
featureIds: FeatureId[];
|
||||
}): Promise<Record<string, ResolvedFeatureState>>;
|
||||
listFeaturesForUser(input: { userId: number }): Promise<ResolvedFeatureState[]>;
|
||||
listFeaturesForTeam(input: {
|
||||
teamId: number;
|
||||
parentOrgId?: number | null;
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getTeamFeatureRepository } from "@calcom/features/di/containers/TeamFea
|
||||
import { getUserFeatureRepository } from "@calcom/features/di/containers/UserFeatureRepository";
|
||||
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||
import { isOptInFeature } from "@calcom/features/feature-opt-in/config";
|
||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import type { ZodEnum } from "zod";
|
||||
@@ -22,27 +21,6 @@ const featureOptInService: ReturnType<typeof getFeatureOptInService> = getFeatur
|
||||
const teamFeatureRepository: ReturnType<typeof getTeamFeatureRepository> = getTeamFeatureRepository();
|
||||
const userFeatureRepository: ReturnType<typeof getUserFeatureRepository> = getUserFeatureRepository();
|
||||
const teamRepository: TeamRepository = new TeamRepository(prisma);
|
||||
const membershipRepository: MembershipRepository = new MembershipRepository(prisma);
|
||||
|
||||
async function getUserOrgAndTeamIds(userId: number): Promise<{ orgId: number | null; teamIds: number[] }> {
|
||||
const memberships = await membershipRepository.findAllByUserId({
|
||||
userId,
|
||||
filters: { accepted: true },
|
||||
});
|
||||
|
||||
let orgId: number | null = null;
|
||||
const teamIds: number[] = [];
|
||||
|
||||
for (const membership of memberships) {
|
||||
if (membership.team.isOrganization) {
|
||||
orgId = membership.teamId;
|
||||
} else {
|
||||
teamIds.push(membership.teamId);
|
||||
}
|
||||
}
|
||||
|
||||
return { orgId, teamIds };
|
||||
}
|
||||
|
||||
export const featureOptInRouter = router({
|
||||
/**
|
||||
@@ -50,12 +28,8 @@ export const featureOptInRouter = router({
|
||||
* This considers all teams/orgs the user belongs to.
|
||||
*/
|
||||
listForUser: authedProcedure.query(async ({ ctx }) => {
|
||||
const { orgId, teamIds } = await getUserOrgAndTeamIds(ctx.user.id);
|
||||
|
||||
return featureOptInService.listFeaturesForUser({
|
||||
userId: ctx.user.id,
|
||||
orgId,
|
||||
teamIds,
|
||||
});
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user