-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Store the organization id in credentials #5002
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,12 @@ export class AuthService extends EventEmitter<AuthServiceEvents> { | |
const formData = new URLSearchParams() | ||
formData.append("_is_native", "1") | ||
|
||
// Only add organization_id if not null (personal accounts) | ||
const organizationId = this.getStoredOrganizationId() | ||
if (organizationId !== null) { | ||
formData.append("organization_id", organizationId) | ||
} | ||
|
||
const response = await fetch(`${getClerkBaseUrl()}/v1/client/sessions/${this.credentials!.sessionId}/tokens`, { | ||
method: "POST", | ||
headers: { | ||
|
@@ -505,23 +538,37 @@ export class AuthService extends EventEmitter<AuthServiceEvents> { | |
|
||
userInfo.picture = userData.image_url | ||
|
||
// Fetch organization memberships separately | ||
// Fetch organization info separately - but only populate if user is in organization context | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is: Fetch organization info if user is in organization context? (That is: we don't fetch if we're not in org context, which is great). |
||
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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar comment to the fetch, but for different reasons: 3 cases again:
(Actually, there is another option here: we could parse the session JWT and look at the |
||
|
||
if (storedOrgId !== null) { | ||
// User is in organization context - fetch user's memberships and filter | ||
const orgMemberships = await this.clerkGetOrganizationMemberships() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could more simply just fetch https://clerk.com/docs/reference/frontend-api/tag/Organization#operation/getOrganization but I assume we plan to have a feature that would benefit from the role "soon enough" that it's worth continuing to fetch the memberships. |
||
|
||
// Find the user's membership in this organization | ||
const userMembership = orgMemberships?.find( | ||
(membership: CloudOrganizationMembership) => membership.organization.id === storedOrgId, | ||
) | ||
|
||
if (userMembership) { | ||
userInfo.organizationId = userMembership.organization.id | ||
userInfo.organizationName = userMembership.organization.name | ||
userInfo.organizationRole = userMembership.role | ||
userInfo.organizationImageUrl = userMembership.organization.image_url | ||
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") | ||
} | ||
} 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 | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I am reading https://clerk.com/docs/reference/frontend-api/tag/Sessions#operation/createSessionToken!path=organization_id&t=request correctly (I have not poked around and actually tested this) there are actually 3 cases to consider:
organization_id=THE_ORG_ID
.organization_id=
.organization_id
param at all.