-
Notifications
You must be signed in to change notification settings - Fork 10.4k
feat: orgs conferencing apps #22211
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: orgs conferencing apps #22211
Conversation
Hey there and thank you for opening this pull request! 👋🏼 We require pull request titles to follow the Conventional Commits specification and it looks like your proposed title needs to be adjusted. Details:
|
Graphite Automations"Add consumer team as reviewer" took an action on this PR • (07/02/25)1 reviewer was added to this PR based on Keith Williams's automation. |
✅ No security or compliance issues detected. Reviewed everything up to b270660. Security Overview
Detected Code Changes
Reply to this PR with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cubic found 9 issues across 22 files. Review them in cubic.dev
React with 👍 or 👎 to teach cubic. Tag @cubic-dev-ai
to give specific feedback.
@@ -9,13 +9,15 @@ import http from "../../../lib/http"; | |||
|
|||
export const QUERY_KEY = "get-installed-conferencing-apps"; | |||
|
|||
export const useAtomsGetInstalledConferencingApps = (teamId?: number) => { | |||
export const useAtomsGetInstalledConferencingApps = (teamId?: number, orgId?: number) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function signature now allows both teamId and orgId, but the logic does not handle the case where both are provided. This could lead to ambiguous or unintended API calls. Consider validating that only one of teamId or orgId is provided at a time.
packages/app-store/server.ts
Outdated
@@ -61,6 +61,14 @@ export async function getLocationGroupedOptions( | |||
id: userOrTeamId.userId, | |||
}, | |||
}); | |||
|
|||
if (user?.organizationId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the user belongs to an organization this branch replaces the previous { userId }
filter with { teamId: { in: [user.organizationId] } }
, causing personal credentials owned by the user to be ignored. As a result users who are part of an org will no longer see their own conferencing credentials in the location selector.
apps/api/v2/src/modules/atoms/controllers/atoms.conferencing-apps.controller.ts
Show resolved
Hide resolved
apps/api/v2/src/modules/atoms/controllers/atoms.conferencing-apps.controller.ts
Outdated
Show resolved
Hide resolved
@@ -169,6 +169,21 @@ export class ConferencingController { | |||
const fallbackUrl = decodedCallbackState.onErrorReturnTo || ""; | |||
return { url: fallbackUrl }; | |||
} | |||
} else if (decodedCallbackState.orgId) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new branch repeats almost the same logic as the preceding team-level branch (building the same params / headers, performing the same axios call, and handling identical success & error flows). Copy-pasting this block increases maintenance cost and risks future inconsistencies. Extract the shared logic into a reusable helper instead of duplicating it.
packages/platform/atoms/connect/conferencing-apps/hooks/useGetDefaultConferencingApp.ts
Show resolved
Hide resolved
This reverts commit 82db763.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
♻️ Duplicate comments (1)
packages/app-store/server.ts (1)
72-83
: Verify the credential filtering logic for organization users.The current implementation replaces the original
{ userId }
filter with team-based filtering when the user has teams. This could cause personal user credentials to be excluded for organization members, potentially breaking existing functionality.The past review comment correctly identified this issue: "When the user belongs to an organization this branch replaces the previous
{ userId }
filter with{ teamId: { in: [user.organizationId] } }
, causing personal credentials owned by the user to be ignored."Run this script to verify the credential filtering behavior:
#!/bin/bash # Description: Verify that personal user credentials are still accessible when user has teams. # Expected: Personal credentials should remain accessible alongside team credentials. # Search for similar patterns in the codebase to understand intended behavior ast-grep --pattern $'{ OR: [ { teamId: { in: $$$ } }, $$$ ] }' # Check if there are other places where user credentials are fetched rg -A 10 -B 5 "userId.*OR.*teamId" --type ts
🧹 Nitpick comments (2)
packages/app-store/server.ts (2)
134-137
: Improve readability of the credential mapping.The destructuring assignment in the map function could be clearer by extracting the mapping logic to a separate variable or using more descriptive intermediate steps.
Apply this diff to improve readability:
- for (const { teamName, credentialId } of app.credentials.map((credential) => ({ - teamName: credential.team?.name, - credentialId: credential.id, - }))) { + const credentialMappings = app.credentials.map((credential) => ({ + teamName: credential.team?.name, + credentialId: credential.id, + })); + + for (const { teamName, credentialId } of credentialMappings) {
147-150
: Remove commented deduplication logic or provide explanation.The commented deduplication code suggests this was intentionally disabled, but without context it's unclear if this is temporary or permanent. This could lead to duplicate options in the UI.
Either remove the commented code if it's no longer needed, or add a comment explaining why deduplication was disabled:
- // const existingOption = apps[groupByCategory].find((o) => o.value === option.value); - // if (!existingOption) { apps[groupByCategory] = [...apps[groupByCategory], option]; - // }Or add an explanatory comment:
+ // Deduplication disabled to support multiple credentials per app // const existingOption = apps[groupByCategory].find((o) => o.value === option.value); // if (!existingOption) { apps[groupByCategory] = [...apps[groupByCategory], option]; // }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
packages/app-store/server.ts
(2 hunks)packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts
(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
**/*.ts
: For Prisma queries, only select data you need; never useinclude
, always useselect
Ensure thecredential.key
field is never returned from tRPC endpoints or APIs
Files:
packages/app-store/server.ts
**/*.{ts,tsx}
📄 CodeRabbit Inference Engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()
in hot paths like loops
Files:
packages/app-store/server.ts
🧬 Code Graph Analysis (1)
packages/app-store/server.ts (1)
scripts/prepare-local-for-delegation-credentials-testing.js (1)
user
(15-17)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Install dependencies / Yarn install & cache
- GitHub Check: Atoms E2E Tests
include: { | ||
teams: { | ||
select: { | ||
teamId: true, | ||
}, | ||
}, | ||
}, | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Include only necessary fields to optimize query performance.
The query includes the entire teams relationship when only teamId
is needed. Following the coding guidelines for Prisma queries, use select
instead of include
to fetch only required data.
Apply this diff to optimize the query:
- include: {
- teams: {
- select: {
- teamId: true,
- },
- },
- },
+ select: {
+ id: true,
+ teams: {
+ select: {
+ teamId: true,
+ },
+ },
+ },
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
include: { | |
teams: { | |
select: { | |
teamId: true, | |
}, | |
}, | |
}, | |
}); | |
select: { | |
id: true, | |
teams: { | |
select: { | |
teamId: true, | |
}, | |
}, | |
}, | |
}); |
🤖 Prompt for AI Agents
In packages/app-store/server.ts around lines 63 to 70, the Prisma query
currently uses include to load the entire teams relation though only teamId is
needed; replace the include with a select for the teams relation (teams: {
select: { teamId: true } }) so only teamId is fetched, and preserve any other
top-level selected fields in the query to avoid loading unnecessary data.
...(app.credential | ||
? { credentialId: app.credential.id, teamName: app.credential.team?.name ?? null } | ||
: {}), | ||
...(app.credential ? { credentialId: credentialId, teamName: teamName ?? null } : {}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix conditional logic for credential properties.
The condition app.credential
doesn't match the loop structure that iterates over app.credentials
(plural). This could cause credential-specific properties to be incorrectly applied.
Apply this diff to fix the conditional logic:
- ...(app.credential ? { credentialId: credentialId, teamName: teamName ?? null } : {}),
+ ...(credentialId ? { credentialId: credentialId, teamName: teamName ?? null } : {}),
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
...(app.credential ? { credentialId: credentialId, teamName: teamName ?? null } : {}), | |
...(credentialId ? { credentialId: credentialId, teamName: teamName ?? null } : {}), |
🤖 Prompt for AI Agents
In packages/app-store/server.ts around line 144, the spread uses app.credential
(singular) which doesn't match the surrounding loop over app.credentials;
replace that conditional with a check for app.credentials existing/non-empty
(for example app.credentials?.length > 0) so the credentialId and teamName
properties are only applied when the app has credentials, e.g. change
...(app.credential ? { credentialId: credentialId, teamName: teamName ?? null }
: {}) to use ...(app.credentials?.length > 0 ? { credentialId, teamName:
teamName ?? null } : {}) so the logic aligns with the plural credentials
structure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lots of CI actions are failing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
api endpoint to list all the conferencing apps for in the conferencing atom for an org
@@ -18,6 +18,7 @@ export class ConferencingAtomsService { | |||
input: { | |||
variant: "conferencing", | |||
onlyInstalled: true, | |||
includeTeamInstalledApps: true, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Display the installed team or organizational conferencing apps in the conferencing atom when viewed at the user level.
@@ -169,6 +172,21 @@ export class ConferencingController { | |||
const fallbackUrl = decodedCallbackState.onErrorReturnTo || ""; | |||
return { url: fallbackUrl }; | |||
} | |||
} else if (decodedCallbackState.orgId) { | |||
const apiUrl = this.config.get("api.url"); | |||
const url = `${apiUrl}/organizations/${decodedCallbackState.orgId}/conferencing/${app}/oauth/callback`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similar to this #20540 (comment)
@@ -216,6 +236,32 @@ export class ConferencingController { | |||
return { status: SUCCESS_STATUS }; | |||
} | |||
|
|||
@Post("/:app/default/:credentialId") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Setting a default app now requires us to include the credentialId, as previously we only had one instance of an app. but now with the introduction of multiple instances at the org, team, and user levels, we need to specify the credentialId to identify which app we're referring to.
if (!CONFERENCING_APPS.includes(appSlug)) { | ||
throw new BadRequestException("Invalid app, available apps are: ", CONFERENCING_APPS.join(", ")); | ||
} | ||
const credentials = await getUsersCredentialsIncludeServiceAccountKey(user); | ||
const credentials = await getUsersAndTeamsCredentialsIncludeServiceAccountKey(user); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added a new function that will also include the credentials for the teams and organizations the user is a part of.
@@ -112,12 +123,17 @@ export class ConferencingService { | |||
}); | |||
} | |||
|
|||
async setDefaultConferencingApp(user: UserWithProfile, app: string) { | |||
async setDefaultConferencingApp(user: UserWithProfile, app: string, credentialId?: number) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
similar to #22211 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This controller handles all the conferencing endpoints for organizations
) {} | ||
|
||
@Roles("TEAM_ADMIN") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed the endpoint for google-meet as we cannot install gogle-meet on teams/orgs
@@ -328,6 +328,7 @@ export const createdEventSchema = z | |||
const schemaDefaultConferencingApp = z.object({ | |||
appSlug: z.string().default("daily-video").optional(), | |||
appLink: z.string().optional(), | |||
credentialId: z.number().optional(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to save the credentialId for the default conferencing app. Currently, Zoom can be installed at the team, organization, or user level, so we need to specify which level's credential to save.
}: { | ||
installedApps?: RouterOutputs["viewer"]["apps"]["integrations"]["items"]; | ||
}) => { | ||
const baseApps = teamId || orgId ? [ZOOM, OFFICE_365_VIDEO] : [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can't have g-meet on team/orgs
@@ -328,6 +328,7 @@ export const createdEventSchema = z | |||
const schemaDefaultConferencingApp = z.object({ | |||
appSlug: z.string().default("daily-video").optional(), | |||
appLink: z.string().optional(), | |||
credentialId: z.number().optional(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need to save the credentialId for the default conferencing app. Currently, Zoom can be installed at the team, organization, or user level, so we need to specify which level's credential to save.
}: { | ||
installedApps?: RouterOutputs["viewer"]["apps"]["integrations"]["items"]; | ||
}) => { | ||
const baseApps = teamId || orgId ? [ZOOM, OFFICE_365_VIDEO] : [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can install google-meet on orgs/teams
apps, | ||
disableBulkUpdateEventTypes = false, | ||
}: ConferencingAppsViewPlatformWrapperProps) => { | ||
const { t } = useLocale(); | ||
const queryClient = useQueryClient(); | ||
const { toast } = useToast(); | ||
const shouldDisableBulkUpdates = !teamId && orgId ? false : disableBulkUpdateEventTypes; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
disable bulkUpdate event type modal for orgs/teams
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts (1)
77-89
: Repeat the DRY + mockResolvedValue refactor in all similar blocksApply the same
setUserRepoProfile(...)
usage to these blocks to remove duplication and ensure async-consistent stubbing.Also applies to: 162-173, 239-250, 343-355, 666-678
🧹 Nitpick comments (1)
packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts (1)
22-34
: DRY the UserRepository mocking and prefer mockResolvedValue for async semanticsYou repeatedly stub
enrichUserWithItsProfileSkipPlatformCheck
and wiremockImplementation
inline. This is verbose and slightly misleading since the repo call is async. Suggest centralizing with a helper and usingmockResolvedValue
.
- Benefit: less duplication, clearer intent, easier to tweak across all tests.
Apply this diff to replace the inline stub in this block:
- const mockEnrichUserWithItsProfileSkipPlatformCheck = vi.fn().mockReturnValue({ - profile: null, - }); - - const mockUserRepository = vi.mocked(UserRepository); - if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") { - mockUserRepository.mockImplementation( - () => - ({ - enrichUserWithItsProfileSkipPlatformCheck: mockEnrichUserWithItsProfileSkipPlatformCheck, - } as any) - ); - } + setUserRepoProfile(null);Add this helper near the top of the file (after the
vi.mock(...)
):function setUserRepoProfile(profile: { organizationId?: number } | null) { const mockedCtor = vi.mocked(UserRepository); if (mockedCtor && typeof mockedCtor.mockImplementation === "function") { mockedCtor.mockImplementation( () => ({ enrichUserWithItsProfileSkipPlatformCheck: vi.fn().mockResolvedValue({ profile }), } as any) ); } }Also consider adding test hygiene:
afterEach(() => { vi.clearAllMocks(); });
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
packages/app-store/server.ts
(2 hunks)packages/features/apps/components/AppList.tsx
(4 hunks)packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts
(12 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
- packages/app-store/server.ts
- packages/features/apps/components/AppList.tsx
🧰 Additional context used
📓 Path-based instructions (3)
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts
: For Prisma queries, only select data you need; never useinclude
, always useselect
Ensure thecredential.key
field is never returned from tRPC endpoints or APIs
Files:
packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()
in hot paths like loops
Files:
packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
- GitHub Check: Production builds / Build Docs
- GitHub Check: Production builds / Build Web App
- GitHub Check: Production builds / Build Atoms
- GitHub Check: Production builds / Build API v2
- GitHub Check: Production builds / Build API v1
- GitHub Check: Tests / Unit
- GitHub Check: Type check / check-types
- GitHub Check: Linters / lint
🔇 Additional comments (2)
packages/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials.test.ts (2)
12-18
: Mocks updated to SkipPlatformCheck API — looks correctTop-level module mock aligns with the production rename to
enrichUserWithItsProfileSkipPlatformCheck
. No issues spotted.
343-345
: Verified — mock matches repository contractThe repo's enrichUserWithItsProfileSkipPlatformCheck populates a profile containing organizationId (number | null) and an optional nested organization object, so mocking profile: { organizationId: orgId } is valid for tests that only rely on the org id.
Key evidence:
- packages/lib/server/repository/user.ts — function enrichUserWithItsProfileSkipPlatformCheck (around line 468) and the returned/selected shape includes "organizationId: number | null" (see lines ~579–581).
- packages/types/UserProfile.d.ts — defines organizationId on Profile (line ~11).
Note: if a test also reads profile.organization or fields on it, add an organization: {...} entry to the mock.
What does this PR do?
This pull request introduces support for installing conferencing applications at the team/organization level.
Benefits
No individual setup required: Users within a team or organization no longer need to connect their personal accounts to Zoom or other conferencing apps.
Shared credentials: Once installed by a team admin, the conferencing app credentials are available for all sub-teams and users within the org.
Users can leverage the shared team/org-installed credentials when creating personal events, as well as sub-team events.
Reduced redundancy: Instead of every user configuring the same app, the admin installs it once and all members can use it seamlessly.
Mandatory Tasks (DO NOT REMOVE)