From 5ce429caa658c0c2aaebb7e0dfc383e0671773cd Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 16 Jun 2025 14:29:05 -0400 Subject: [PATCH 1/3] Fetch organization info in the extension --- packages/cloud/src/AuthService.ts | 35 +++++++++- packages/cloud/src/CloudService.ts | 18 +++++ .../cloud/src/__tests__/CloudService.test.ts | 66 +++++++++++++++++++ packages/types/src/cloud.ts | 31 +++++++++ .../src/components/account/AccountView.tsx | 5 ++ 5 files changed, 154 insertions(+), 1 deletion(-) diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index fda7df79455..74839fc7bfb 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -5,7 +5,7 @@ import axios from "axios" import * as vscode from "vscode" import { z } from "zod" -import type { CloudUserInfo } from "@roo-code/types" +import type { CloudUserInfo, ClerkOrganizationMembership } from "@roo-code/types" import { getClerkBaseUrl, getRooCodeApiUrl } from "./Config" import { RefreshTimer } from "./RefreshTimer" @@ -420,9 +420,42 @@ export class AuthService extends EventEmitter { } userInfo.picture = userData?.image_url + + // Fetch organization memberships separately + try { + const orgMemberships = await this.clerkGetOrganizationMemberships() + if (orgMemberships && orgMemberships.length > 0) { + // Get the first (or active) organization membership + const primaryOrgMembership = orgMemberships[0] + const organization = primaryOrgMembership?.organization + + if (organization) { + userInfo.organizationId = organization.id + userInfo.organizationName = organization.name + userInfo.organizationRole = primaryOrgMembership.role + } + } + } catch (error) { + this.log("[auth] Failed to fetch organization memberships:", error) + // Don't throw - organization info is optional + } + return userInfo } + private async clerkGetOrganizationMemberships(): Promise { + const response = await axios.get(`${getClerkBaseUrl()}/v1/me/organization_memberships`, { + headers: { + Authorization: `Bearer ${this.credentials!.clientToken}`, + "User-Agent": this.userAgent(), + }, + }) + + // The response structure is: { response: [...] } + // Extract the organization memberships from the response.response array + return response.data?.response || [] + } + private async clerkLogout(credentials: AuthCredentials): Promise { const formData = new URLSearchParams() formData.append("_is_native", "1") diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index 08a270bfc30..fe3bad970ce 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -92,6 +92,24 @@ export class CloudService { return this.authService!.getUserInfo() } + public getOrganizationId(): string | null { + this.ensureInitialized() + const userInfo = this.authService!.getUserInfo() + return userInfo?.organizationId || null + } + + public getOrganizationName(): string | null { + this.ensureInitialized() + const userInfo = this.authService!.getUserInfo() + return userInfo?.organizationName || null + } + + public getOrganizationRole(): string | null { + this.ensureInitialized() + const userInfo = this.authService!.getUserInfo() + return userInfo?.organizationRole || null + } + public getAuthState(): string { this.ensureInitialized() return this.authService!.getState() diff --git a/packages/cloud/src/__tests__/CloudService.test.ts b/packages/cloud/src/__tests__/CloudService.test.ts index 8e6ca983138..03b28568d50 100644 --- a/packages/cloud/src/__tests__/CloudService.test.ts +++ b/packages/cloud/src/__tests__/CloudService.test.ts @@ -184,6 +184,72 @@ describe("CloudService", () => { expect(mockAuthService.getUserInfo).toHaveBeenCalled() }) + it("should return organization ID from user info", () => { + const mockUserInfo = { + name: "Test User", + email: "test@example.com", + organizationId: "org_123", + organizationName: "Test Org", + organizationRole: "admin", + } + mockAuthService.getUserInfo.mockReturnValue(mockUserInfo) + + const result = cloudService.getOrganizationId() + expect(mockAuthService.getUserInfo).toHaveBeenCalled() + expect(result).toBe("org_123") + }) + + it("should return null when no organization ID available", () => { + mockAuthService.getUserInfo.mockReturnValue(null) + + const result = cloudService.getOrganizationId() + expect(result).toBe(null) + }) + + it("should return organization name from user info", () => { + const mockUserInfo = { + name: "Test User", + email: "test@example.com", + organizationId: "org_123", + organizationName: "Test Org", + organizationRole: "admin", + } + mockAuthService.getUserInfo.mockReturnValue(mockUserInfo) + + const result = cloudService.getOrganizationName() + expect(mockAuthService.getUserInfo).toHaveBeenCalled() + expect(result).toBe("Test Org") + }) + + it("should return null when no organization name available", () => { + mockAuthService.getUserInfo.mockReturnValue(null) + + const result = cloudService.getOrganizationName() + expect(result).toBe(null) + }) + + it("should return organization role from user info", () => { + const mockUserInfo = { + name: "Test User", + email: "test@example.com", + organizationId: "org_123", + organizationName: "Test Org", + organizationRole: "admin", + } + mockAuthService.getUserInfo.mockReturnValue(mockUserInfo) + + const result = cloudService.getOrganizationRole() + expect(mockAuthService.getUserInfo).toHaveBeenCalled() + expect(result).toBe("admin") + }) + + it("should return null when no organization role available", () => { + mockAuthService.getUserInfo.mockReturnValue(null) + + const result = cloudService.getOrganizationRole() + expect(result).toBe(null) + }) + it("should delegate getAuthState to AuthService", () => { const result = cloudService.getAuthState() expect(mockAuthService.getState).toHaveBeenCalled() diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 6347d596fe6..97c5dcf3be7 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -10,6 +10,37 @@ export interface CloudUserInfo { name?: string email?: string picture?: string + organizationId?: string + organizationName?: string + organizationRole?: string +} + +/** + * Clerk Organization Membership Types + */ + +export interface ClerkOrganization { + id: string + name: string + slug?: string + image_url?: string + has_image?: boolean + created_at?: number + updated_at?: number +} + +export interface ClerkOrganizationMembership { + id: string + organization: ClerkOrganization + role: string + permissions?: string[] + created_at?: number + updated_at?: number +} + +export interface ClerkOrganizationMembershipsResponse { + data: ClerkOrganizationMembership[] + total_count?: number } /** diff --git a/webview-ui/src/components/account/AccountView.tsx b/webview-ui/src/components/account/AccountView.tsx index 1468caa58ec..04d6cb7d2bb 100644 --- a/webview-ui/src/components/account/AccountView.tsx +++ b/webview-ui/src/components/account/AccountView.tsx @@ -44,6 +44,11 @@ export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewPr

{userInfo?.name || t("account:unknownUser")}

+ {userInfo?.organizationName && ( +

+ {userInfo.organizationName} +

+ )}

{userInfo?.email || ""}

)} From 590258c90bbe79ae59e3d9cbdedccfe556347a02 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 16 Jun 2025 14:48:30 -0400 Subject: [PATCH 2/3] Update type names --- packages/cloud/src/AuthService.ts | 4 ++-- packages/types/src/cloud.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cloud/src/AuthService.ts b/packages/cloud/src/AuthService.ts index 74839fc7bfb..68036ce3c97 100644 --- a/packages/cloud/src/AuthService.ts +++ b/packages/cloud/src/AuthService.ts @@ -5,7 +5,7 @@ import axios from "axios" import * as vscode from "vscode" import { z } from "zod" -import type { CloudUserInfo, ClerkOrganizationMembership } from "@roo-code/types" +import type { CloudUserInfo, CloudOrganizationMembership } from "@roo-code/types" import { getClerkBaseUrl, getRooCodeApiUrl } from "./Config" import { RefreshTimer } from "./RefreshTimer" @@ -443,7 +443,7 @@ export class AuthService extends EventEmitter { return userInfo } - private async clerkGetOrganizationMemberships(): Promise { + private async clerkGetOrganizationMemberships(): Promise { const response = await axios.get(`${getClerkBaseUrl()}/v1/me/organization_memberships`, { headers: { Authorization: `Bearer ${this.credentials!.clientToken}`, diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 97c5dcf3be7..50b49ad0d26 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -16,10 +16,10 @@ export interface CloudUserInfo { } /** - * Clerk Organization Membership Types + * CloudOrganization Types */ -export interface ClerkOrganization { +export interface CloudOrganization { id: string name: string slug?: string @@ -29,17 +29,17 @@ export interface ClerkOrganization { updated_at?: number } -export interface ClerkOrganizationMembership { +export interface CloudOrganizationMembership { id: string - organization: ClerkOrganization + organization: CloudOrganization role: string permissions?: string[] created_at?: number updated_at?: number } -export interface ClerkOrganizationMembershipsResponse { - data: ClerkOrganizationMembership[] +export interface CloudOrganizationMembershipsResponse { + data: CloudOrganizationMembership[] total_count?: number } From 68d02540788bbaf20ce4d7b19a8b9b8355a74037 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Mon, 16 Jun 2025 14:50:33 -0400 Subject: [PATCH 3/3] Remove unused --- packages/types/src/cloud.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/types/src/cloud.ts b/packages/types/src/cloud.ts index 50b49ad0d26..6f5547b3d55 100644 --- a/packages/types/src/cloud.ts +++ b/packages/types/src/cloud.ts @@ -38,11 +38,6 @@ export interface CloudOrganizationMembership { updated_at?: number } -export interface CloudOrganizationMembershipsResponse { - data: CloudOrganizationMembership[] - total_count?: number -} - /** * OrganizationAllowList */