|
|
@@ -21,6 +21,7 @@ export interface AuthServiceEvents {
|
|
|
const authCredentialsSchema = z.object({
|
|
|
clientToken: z.string().min(1, "Client token cannot be empty"),
|
|
|
sessionId: z.string().min(1, "Session ID cannot be empty"),
|
|
|
+ organizationId: z.string().nullable().optional(),
|
|
|
})
|
|
|
|
|
|
type AuthCredentials = z.infer<typeof authCredentialsSchema>
|
|
|
@@ -220,7 +221,16 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|
|
|
|
|
try {
|
|
|
const parsedJson = JSON.parse(credentialsJson)
|
|
|
- return authCredentialsSchema.parse(parsedJson)
|
|
|
+ const credentials = authCredentialsSchema.parse(parsedJson)
|
|
|
+
|
|
|
+ // Migration: If no organizationId but we have userInfo, add it
|
|
|
+ if (credentials.organizationId === undefined && this.userInfo?.organizationId) {
|
|
|
+ credentials.organizationId = this.userInfo.organizationId
|
|
|
+ await this.storeCredentials(credentials)
|
|
|
+ this.log("[auth] Migrated credentials with organizationId")
|
|
|
+ }
|
|
|
+
|
|
|
+ return credentials
|
|
|
} catch (error) {
|
|
|
if (error instanceof z.ZodError) {
|
|
|
this.log("[auth] Invalid credentials format:", error.errors)
|
|
|
@@ -269,8 +279,13 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|
|
*
|
|
|
* @param code The authorization code from the callback
|
|
|
* @param state The state parameter from the callback
|
|
|
+ * @param organizationId The organization ID from the callback (null for personal accounts)
|
|
|
*/
|
|
|
- public async handleCallback(code: string | null, state: string | null): Promise<void> {
|
|
|
+ public async handleCallback(
|
|
|
+ code: string | null,
|
|
|
+ state: string | null,
|
|
|
+ organizationId?: string | null,
|
|
|
+ ): Promise<void> {
|
|
|
if (!code || !state) {
|
|
|
vscode.window.showInformationMessage("Invalid Roo Code Cloud sign in url")
|
|
|
return
|
|
|
@@ -287,6 +302,9 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|
|
|
|
|
const credentials = await this.clerkSignIn(code)
|
|
|
|
|
|
+ // Set organizationId (null for personal accounts)
|
|
|
+ credentials.organizationId = organizationId || null
|
|
|
+
|
|
|
await this.storeCredentials(credentials)
|
|
|
|
|
|
vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud")
|
|
|
@@ -417,6 +435,15 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|
|
return this.userInfo
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Get the stored organization ID from credentials
|
|
|
+ *
|
|
|
+ * @returns The stored organization ID, null for personal accounts or if no credentials exist
|
|
|
+ */
|
|
|
+ public getStoredOrganizationId(): string | null {
|
|
|
+ return this.credentials?.organizationId || null
|
|
|
+ }
|
|
|
+
|
|
|
private async clerkSignIn(ticket: string): Promise<AuthCredentials> {
|
|
|
const formData = new URLSearchParams()
|
|
|
formData.append("strategy", "ticket")
|
|
|
@@ -454,6 +481,17 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|
|
const formData = new URLSearchParams()
|
|
|
formData.append("_is_native", "1")
|
|
|
|
|
|
+ // Handle 3 cases for organization_id:
|
|
|
+ // 1. Have an org id: organization_id=THE_ORG_ID
|
|
|
+ // 2. Have a personal account: organization_id= (empty string)
|
|
|
+ // 3. Don't know if you have an org id (old style credentials): don't send organization_id param at all
|
|
|
+ const organizationId = this.getStoredOrganizationId()
|
|
|
+ if (this.credentials?.organizationId !== undefined) {
|
|
|
+ // We have organization context info (either org id or personal account)
|
|
|
+ formData.append("organization_id", organizationId || "")
|
|
|
+ }
|
|
|
+ // If organizationId is undefined, don't send the param at all (old credentials)
|
|
|
+
|
|
|
const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, {
|
|
|
method: "POST",
|
|
|
headers: {
|
|
|
@@ -505,29 +543,74 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
|
|
|
|
|
|
userInfo.picture = userData.image_url
|
|
|
|
|
|
- // Fetch organization memberships separately
|
|
|
+ // Fetch organization info if user is in organization context
|
|
|
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
|
|
|
- userInfo.organizationImageUrl = organization.image_url
|
|
|
+ const storedOrgId = this.getStoredOrganizationId()
|
|
|
+
|
|
|
+ if (this.credentials?.organizationId !== undefined) {
|
|
|
+ // We have organization context info
|
|
|
+ if (storedOrgId !== null) {
|
|
|
+ // User is in organization context - fetch user's memberships and filter
|
|
|
+ const orgMemberships = await this.clerkGetOrganizationMemberships()
|
|
|
+ const userMembership = this.findOrganizationMembership(orgMemberships, storedOrgId)
|
|
|
+
|
|
|
+ if (userMembership) {
|
|
|
+ this.setUserOrganizationInfo(userInfo, userMembership)
|
|
|
+ this.log("[auth] User in organization context:", {
|
|
|
+ id: userMembership.organization.id,
|
|
|
+ name: userMembership.organization.name,
|
|
|
+ role: userMembership.role,
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.log("[auth] Warning: User not found in stored organization:", storedOrgId)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ this.log("[auth] User in personal account context - not setting organization info")
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // Old credentials without organization context - fetch organization info to determine context
|
|
|
+ const orgMemberships = await this.clerkGetOrganizationMemberships()
|
|
|
+ const primaryOrgMembership = this.findPrimaryOrganizationMembership(orgMemberships)
|
|
|
+
|
|
|
+ if (primaryOrgMembership) {
|
|
|
+ this.setUserOrganizationInfo(userInfo, primaryOrgMembership)
|
|
|
+ this.log("[auth] Legacy credentials: Found organization membership:", {
|
|
|
+ id: primaryOrgMembership.organization.id,
|
|
|
+ name: primaryOrgMembership.organization.name,
|
|
|
+ role: primaryOrgMembership.role,
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.log("[auth] Legacy credentials: No organization memberships found")
|
|
|
}
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- this.log("[auth] Failed to fetch organization memberships:", error)
|
|
|
+ this.log("[auth] Failed to fetch organization info:", error)
|
|
|
// Don't throw - organization info is optional
|
|
|
}
|
|
|
|
|
|
return userInfo
|
|
|
}
|
|
|
|
|
|
+ private findOrganizationMembership(
|
|
|
+ memberships: CloudOrganizationMembership[],
|
|
|
+ organizationId: string,
|
|
|
+ ): CloudOrganizationMembership | undefined {
|
|
|
+ return memberships?.find((membership) => membership.organization.id === organizationId)
|
|
|
+ }
|
|
|
+
|
|
|
+ private findPrimaryOrganizationMembership(
|
|
|
+ memberships: CloudOrganizationMembership[],
|
|
|
+ ): CloudOrganizationMembership | undefined {
|
|
|
+ return memberships && memberships.length > 0 ? memberships[0] : undefined
|
|
|
+ }
|
|
|
+
|
|
|
+ private setUserOrganizationInfo(userInfo: CloudUserInfo, membership: CloudOrganizationMembership): void {
|
|
|
+ userInfo.organizationId = membership.organization.id
|
|
|
+ userInfo.organizationName = membership.organization.name
|
|
|
+ userInfo.organizationRole = membership.role
|
|
|
+ userInfo.organizationImageUrl = membership.organization.image_url
|
|
|
+ }
|
|
|
+
|
|
|
private async clerkGetOrganizationMemberships(): Promise<CloudOrganizationMembership[]> {
|
|
|
const response = await fetch(`${getClerkBaseUrl()}/v1/me/organization_memberships`, {
|
|
|
headers: {
|