Procházet zdrojové kódy

Fetch organization info in the extension (#4751)

Matt Rubens před 6 měsíci
rodič
revize
322a15ee5d

+ 34 - 1
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, CloudOrganizationMembership } from "@roo-code/types"
 
 import { getClerkBaseUrl, getRooCodeApiUrl } from "./Config"
 import { RefreshTimer } from "./RefreshTimer"
@@ -420,9 +420,42 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
 		}
 
 		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<CloudOrganizationMembership[]> {
+		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<void> {
 		const formData = new URLSearchParams()
 		formData.append("_is_native", "1")

+ 18 - 0
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()

+ 66 - 0
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: "[email protected]",
+				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: "[email protected]",
+				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: "[email protected]",
+				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()

+ 26 - 0
packages/types/src/cloud.ts

@@ -10,6 +10,32 @@ export interface CloudUserInfo {
 	name?: string
 	email?: string
 	picture?: string
+	organizationId?: string
+	organizationName?: string
+	organizationRole?: string
+}
+
+/**
+ * CloudOrganization Types
+ */
+
+export interface CloudOrganization {
+	id: string
+	name: string
+	slug?: string
+	image_url?: string
+	has_image?: boolean
+	created_at?: number
+	updated_at?: number
+}
+
+export interface CloudOrganizationMembership {
+	id: string
+	organization: CloudOrganization
+	role: string
+	permissions?: string[]
+	created_at?: number
+	updated_at?: number
 }
 
 /**

+ 5 - 0
webview-ui/src/components/account/AccountView.tsx

@@ -44,6 +44,11 @@ export const AccountView = ({ userInfo, isAuthenticated, onDone }: AccountViewPr
 							<h2 className="text-lg font-medium text-vscode-foreground mb-1">
 								{userInfo?.name || t("account:unknownUser")}
 							</h2>
+							{userInfo?.organizationName && (
+								<p className="text-sm text-vscode-descriptionForeground mb-1">
+									{userInfo.organizationName}
+								</p>
+							)}
 							<p className="text-sm text-vscode-descriptionForeground">{userInfo?.email || ""}</p>
 						</div>
 					)}