Skip to content

Commit 322a15e

Browse files
authored
Fetch organization info in the extension (#4751)
1 parent 2e2f83b commit 322a15e

File tree

5 files changed

+149
-1
lines changed

5 files changed

+149
-1
lines changed

packages/cloud/src/AuthService.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import axios from "axios"
55
import * as vscode from "vscode"
66
import { z } from "zod"
77

8-
import type { CloudUserInfo } from "@roo-code/types"
8+
import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types"
99

1010
import { getClerkBaseUrl, getRooCodeApiUrl } from "./Config"
1111
import { RefreshTimer } from "./RefreshTimer"
@@ -420,9 +420,42 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
420420
}
421421

422422
userInfo.picture = userData?.image_url
423+
424+
// Fetch organization memberships separately
425+
try {
426+
const orgMemberships = await this.clerkGetOrganizationMemberships()
427+
if (orgMemberships && orgMemberships.length > 0) {
428+
// Get the first (or active) organization membership
429+
const primaryOrgMembership = orgMemberships[0]
430+
const organization = primaryOrgMembership?.organization
431+
432+
if (organization) {
433+
userInfo.organizationId = organization.id
434+
userInfo.organizationName = organization.name
435+
userInfo.organizationRole = primaryOrgMembership.role
436+
}
437+
}
438+
} catch (error) {
439+
this.log("[auth] Failed to fetch organization memberships:", error)
440+
// Don't throw - organization info is optional
441+
}
442+
423443
return userInfo
424444
}
425445

446+
private async clerkGetOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
447+
const response = await axios.get(`${getClerkBaseUrl()}/v1/me/organization_memberships`, {
448+
headers: {
449+
Authorization: `Bearer ${this.credentials!.clientToken}`,
450+
"User-Agent": this.userAgent(),
451+
},
452+
})
453+
454+
// The response structure is: { response: [...] }
455+
// Extract the organization memberships from the response.response array
456+
return response.data?.response || []
457+
}
458+
426459
private async clerkLogout(credentials: AuthCredentials): Promise<void> {
427460
const formData = new URLSearchParams()
428461
formData.append("_is_native", "1")

packages/cloud/src/CloudService.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,24 @@ export class CloudService {
9292
return this.authService!.getUserInfo()
9393
}
9494

95+
public getOrganizationId(): string | null {
96+
this.ensureInitialized()
97+
const userInfo = this.authService!.getUserInfo()
98+
return userInfo?.organizationId || null
99+
}
100+
101+
public getOrganizationName(): string | null {
102+
this.ensureInitialized()
103+
const userInfo = this.authService!.getUserInfo()
104+
return userInfo?.organizationName || null
105+
}
106+
107+
public getOrganizationRole(): string | null {
108+
this.ensureInitialized()
109+
const userInfo = this.authService!.getUserInfo()
110+
return userInfo?.organizationRole || null
111+
}
112+
95113
public getAuthState(): string {
96114
this.ensureInitialized()
97115
return this.authService!.getState()

packages/cloud/src/__tests__/CloudService.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,72 @@ describe("CloudService", () => {
184184
expect(mockAuthService.getUserInfo).toHaveBeenCalled()
185185
})
186186

187+
it("should return organization ID from user info", () => {
188+
const mockUserInfo = {
189+
name: "Test User",
190+
191+
organizationId: "org_123",
192+
organizationName: "Test Org",
193+
organizationRole: "admin",
194+
}
195+
mockAuthService.getUserInfo.mockReturnValue(mockUserInfo)
196+
197+
const result = cloudService.getOrganizationId()
198+
expect(mockAuthService.getUserInfo).toHaveBeenCalled()
199+
expect(result).toBe("org_123")
200+
})
201+
202+
it("should return null when no organization ID available", () => {
203+
mockAuthService.getUserInfo.mockReturnValue(null)
204+
205+
const result = cloudService.getOrganizationId()
206+
expect(result).toBe(null)
207+
})
208+
209+
it("should return organization name from user info", () => {
210+
const mockUserInfo = {
211+
name: "Test User",
212+
213+
organizationId: "org_123",
214+
organizationName: "Test Org",
215+
organizationRole: "admin",
216+
}
217+
mockAuthService.getUserInfo.mockReturnValue(mockUserInfo)
218+
219+
const result = cloudService.getOrganizationName()
220+
expect(mockAuthService.getUserInfo).toHaveBeenCalled()
221+
expect(result).toBe("Test Org")
222+
})
223+
224+
it("should return null when no organization name available", () => {
225+
mockAuthService.getUserInfo.mockReturnValue(null)
226+
227+
const result = cloudService.getOrganizationName()
228+
expect(result).toBe(null)
229+
})
230+
231+
it("should return organization role from user info", () => {
232+
const mockUserInfo = {
233+
name: "Test User",
234+
235+
organizationId: "org_123",
236+
organizationName: "Test Org",
237+
organizationRole: "admin",
238+
}
239+
mockAuthService.getUserInfo.mockReturnValue(mockUserInfo)
240+
241+
const result = cloudService.getOrganizationRole()
242+
expect(mockAuthService.getUserInfo).toHaveBeenCalled()
243+
expect(result).toBe("admin")
244+
})
245+
246+
it("should return null when no organization role available", () => {
247+
mockAuthService.getUserInfo.mockReturnValue(null)
248+
249+
const result = cloudService.getOrganizationRole()
250+
expect(result).toBe(null)
251+
})
252+
187253
it("should delegate getAuthState to AuthService", () => {
188254
const result = cloudService.getAuthState()
189255
expect(mockAuthService.getState).toHaveBeenCalled()

packages/types/src/cloud.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,32 @@ export interface CloudUserInfo {
1010
name?: string
1111
email?: string
1212
picture?: string
13+
organizationId?: string
14+
organizationName?: string
15+
organizationRole?: string
16+
}
17+
18+
/**
19+
* CloudOrganization Types
20+
*/
21+
22+
export interface CloudOrganization {
23+
id: string
24+
name: string
25+
slug?: string
26+
image_url?: string
27+
has_image?: boolean
28+
created_at?: number
29+
updated_at?: number
30+
}
31+
32+
export interface CloudOrganizationMembership {
33+
id: string
34+
organization: CloudOrganization
35+
role: string
36+
permissions?: string[]
37+
created_at?: number
38+
updated_at?: number
1339
}
1440

1541
/**

webview-ui/src/components/account/AccountView.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewPr
4444
<h2 className="text-lg font-medium text-vscode-foreground mb-1">
4545
{userInfo?.name || t("account:unknownUser")}
4646
</h2>
47+
{userInfo?.organizationName && (
48+
<p className="text-sm text-vscode-descriptionForeground mb-1">
49+
{userInfo.organizationName}
50+
</p>
51+
)}
4752
<p className="text-sm text-vscode-descriptionForeground">{userInfo?.email || ""}</p>
4853
</div>
4954
)}

0 commit comments

Comments
 (0)