Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
84 changes: 79 additions & 5 deletions apps/code/src/main/services/auth/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,15 +551,23 @@ export class AuthService extends TypedEventEmitter<AuthServiceEvents> {
options: TokenResponseOptions,
): Promise<InMemorySession> {
const scopedOrgIds = tokenResponse.scoped_organizations ?? [];
const scopedTeamIds = tokenResponse.scoped_teams ?? [];
const { accountKey, currentOrgId } = await this.fetchUserContext(
tokenResponse.access_token,
options.cloudRegion,
);
const orgProjectsMap = await this.buildOrgProjectsMap(
tokenResponse.access_token,
options.cloudRegion,
scopedOrgIds,
);
const orgProjectsMap =
scopedOrgIds.length > 0
? await this.buildOrgProjectsMap(
tokenResponse.access_token,
options.cloudRegion,
scopedOrgIds,
)
: await this.buildOrgProjectsMapFromTeams(
tokenResponse.access_token,
options.cloudRegion,
scopedTeamIds,
);
const lastPrefs = accountKey
? this.authPreferenceRepository.get(accountKey, options.cloudRegion)
: null;
Expand Down Expand Up @@ -602,6 +610,72 @@ export class AuthService extends TypedEventEmitter<AuthServiceEvents> {

return Object.fromEntries(entries);
}
private async buildOrgProjectsMapFromTeams(
accessToken: string,
cloudRegion: CloudRegion,
teamIds: number[],
): Promise<OrgProjectsMap> {
if (teamIds.length === 0) return {};

const teamMetas = await Promise.all(
teamIds.map((id) => this.fetchProjectMeta(accessToken, cloudRegion, id)),
);

const projectsByOrg = new Map<string, { id: number; name: string }[]>();
for (const meta of teamMetas) {
if (!meta) continue;
const bucket = projectsByOrg.get(meta.organization) ?? [];
bucket.push({ id: meta.id, name: meta.name });
projectsByOrg.set(meta.organization, bucket);
}

const entries = await Promise.all(
Array.from(projectsByOrg.entries()).map(
async ([orgId, projects]): Promise<[string, OrgProjects]> => {
const org = await this.fetchOrgWithProjects(
accessToken,
cloudRegion,
orgId,
);
return [orgId, { orgName: org?.orgName ?? "(unknown)", projects }];
Comment on lines +635 to +640
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Project-scoped token may always yield "(unknown)" org name

fetchOrgWithProjects calls /api/organizations/${orgId}/ using the same access token. A project-scoped OAuth grant is unlikely to carry permission to read the org endpoint, so res.ok will be false, the function returns null, and every project-scoped login will display "(unknown)" as the org name in the switcher. The PR test plan specifically checks that the org name is visible — worth confirming the token does have org-read access, or falling back to the project's own organization ID as the display label when the org fetch fails.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/auth/service.ts
Line: 635-640

Comment:
**Project-scoped token may always yield "(unknown)" org name**

`fetchOrgWithProjects` calls `/api/organizations/${orgId}/` using the same access token. A project-scoped OAuth grant is unlikely to carry permission to read the org endpoint, so `res.ok` will be `false`, the function returns `null`, and every project-scoped login will display `"(unknown)"` as the org name in the switcher. The PR test plan specifically checks that the org name is visible — worth confirming the token does have org-read access, or falling back to the project's own `organization` ID as the display label when the org fetch fails.

How can I resolve this? If you propose a fix, please make it concise.

},
),
);
Comment on lines +632 to +643
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Superfluous full-org fetch: only orgName is consumed

fetchOrgWithProjects issues a network request to /api/organizations/${orgId}/ and parses the entire teams array — all of which is discarded, since projects in the returned tuple comes from projectsByOrg (the scoped team metas), not from the org call. Consider extracting a thin fetchOrgName helper that skips the teams mapping, or at least skip the call entirely when the org ID can be used as a display label. This follows simplicity rule 4 (no superfluous parts) and avoids the unnecessary allocation on each login.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/code/src/main/services/auth/service.ts
Line: 632-643

Comment:
**Superfluous full-org fetch: only `orgName` is consumed**

`fetchOrgWithProjects` issues a network request to `/api/organizations/${orgId}/` and parses the entire `teams` array — all of which is discarded, since `projects` in the returned tuple comes from `projectsByOrg` (the scoped team metas), not from the org call. Consider extracting a thin `fetchOrgName` helper that skips the `teams` mapping, or at least skip the call entirely when the org ID can be used as a display label. This follows simplicity rule 4 (no superfluous parts) and avoids the unnecessary allocation on each login.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!


return Object.fromEntries(entries);
}
private async fetchProjectMeta(
accessToken: string,
cloudRegion: CloudRegion,
teamId: number,
): Promise<{ id: number; name: string; organization: string } | null> {
const apiHost = getCloudUrlFromRegion(cloudRegion);
try {
const res = await this.executeAuthenticatedFetch(
fetch,
`${apiHost}/api/projects/${teamId}/`,
{},
accessToken,
);
if (!res.ok) return null;
const raw = (await res.json().catch(() => null)) as {
id?: unknown;
name?: unknown;
organization?: unknown;
} | null;
if (
typeof raw?.id !== "number" ||
typeof raw?.name !== "string" ||
typeof raw?.organization !== "string"
) {
return null;
}
return { id: raw.id, name: raw.name, organization: raw.organization };
} catch (error) {
log.warn("Failed to fetch project meta", { teamId, error });
return null;
}
}
private async fetchOrgProjects(
accessToken: string,
cloudRegion: CloudRegion,
Expand Down
1 change: 1 addition & 0 deletions apps/code/src/main/services/oauth/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export const oAuthTokenResponse = z.object({
scope: z.string().optional().default(""),
refresh_token: z.string(),
scoped_organizations: z.array(z.string()).optional(),
scoped_teams: z.array(z.number()).optional(),
});
export type OAuthTokenResponse = z.infer<typeof oAuthTokenResponse>;

Expand Down
Loading