Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion packages/cloud/src/AuthService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down
18 changes: 18 additions & 0 deletions packages/cloud/src/CloudService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
66 changes: 66 additions & 0 deletions packages/cloud/src/__tests__/CloudService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
31 changes: 31 additions & 0 deletions packages/types/src/cloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,37 @@ 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
}

export interface CloudOrganizationMembershipsResponse {
data: CloudOrganizationMembership[]
total_count?: number
}

/**
Expand Down
5 changes: 5 additions & 0 deletions webview-ui/src/components/account/AccountView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
)}
Expand Down