Skip to content
Open
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
17 changes: 8 additions & 9 deletions src/rovo-dev/api/extensionApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,11 @@ export class ExtensionApi {
}
},

saveRovoDevAuthInfo: async (authInfo: any): Promise<void> => {
saveRovoDevAuthInfo: async (authInfo: AuthInfo): Promise<void> => {
await Container.credentialManager.saveRovoDevAuthInfo(authInfo);
},

getRovoDevAuthInfo: async (): Promise<any | undefined> => {
getRovoDevAuthInfo: async (): Promise<AuthInfo | undefined> => {
return await Container.credentialManager.getRovoDevAuthInfo();
},

Expand All @@ -168,12 +168,12 @@ export class ExtensionApi {
},

/**
* Get credential hints (host + email) from all configured Jira sites.
* Get credential hints (email) from all configured Jira sites.
* Used for autocomplete suggestions in credential forms.
*
* @returns Array of unique {host, email} combinations from all sites
* @returns Array of unique {email} objects from all sites
*/
getCredentialHints: async (): Promise<{ host: string; email: string }[]> => {
getCredentialHints: async (): Promise<{ email: string }[]> => {
const sites = Container.siteManager.getSitesAvailable(ProductJira);
const credentialsPromises = sites.map(async (site) => {
try {
Expand All @@ -182,7 +182,6 @@ export class ExtensionApi {
return null;
}
return {
host: site.host,
email: authInfo.user.email,
};
} catch {
Expand All @@ -191,10 +190,10 @@ export class ExtensionApi {
});

const allCredentials = await Promise.all(credentialsPromises);
const credentials = allCredentials.filter((c): c is { host: string; email: string } => c !== null);
const credentials = allCredentials.filter((c): c is { email: string } => c !== null);

// Remove duplicates by creating a unique key
return Array.from(new Map(credentials.map((c) => [`${c.host}-${c.email}`, c])).values());
// Remove duplicates by email
return Array.from(new Map(credentials.map((c) => [c.email, c])).values());
},
};

Expand Down
59 changes: 9 additions & 50 deletions src/rovo-dev/rovoDevAuthValidator.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { AuthInfoState } from 'src/atlclients/authInfo';
import { AuthInfo, AuthInfoState } from 'src/atlclients/authInfo';

import { RovoDevLogger } from './util/rovoDevLogger';

export interface RovoDevAuthInfo {
export interface RovoDevAuthInfo extends AuthInfo {
user: {
id: string;
displayName: string;
Expand All @@ -12,8 +12,7 @@ export interface RovoDevAuthInfo {
state: AuthInfoState;
username: string;
password: string;
host: string;
cloudId: string;
isStaging?: boolean;
}

interface GraphQLUserInfo {
Expand All @@ -25,30 +24,14 @@ interface GraphQLUserInfo {
/**
* Validates RovoDev credentials and creates an AuthInfo object with user information and cloud ID.
*
* @param host - The Atlassian Cloud host (with or without protocol/trailing slashes)
* @param email - The user's email address
* @param apiToken - The API token for authentication
* @returns Promise that resolves to AuthInfo if validation succeeds
* @throws Error with descriptive message if validation fails
*/
export async function createValidatedRovoDevAuthInfo(
host: string,
email: string,
apiToken: string,
): Promise<RovoDevAuthInfo> {
// Normalize host to remove protocol and trailing slashes
const normalizedHost = host.replace(/^https?:\/\//, '').replace(/\/$/, '');

// Validate that it's an atlassian.net domain
if (!normalizedHost.endsWith('.atlassian.net')) {
throw new Error('Please enter a valid Atlassian Cloud site (*.atlassian.net)');
}

export async function createValidatedRovoDevAuthInfo(email: string, apiToken: string): Promise<RovoDevAuthInfo> {
// Fetch user information (validates credentials implicitly)
const userInfo = await fetchUserInfo(normalizedHost, email, apiToken);

// Fetch cloud ID for the site
const cloudId = await fetchCloudId(normalizedHost);
const userInfo = await fetchUserInfo(email, apiToken);

// Create and return AuthInfo with validated information
return {
Expand All @@ -61,18 +44,18 @@ export async function createValidatedRovoDevAuthInfo(
state: AuthInfoState.Valid,
username: email,
password: apiToken,
host: normalizedHost,
cloudId: cloudId,
};
}

/**
* Fetches user information from the GraphQL API using the provided credentials.
* This is done mainly to validate that the credentials are correct.
*/
async function fetchUserInfo(host: string, email: string, apiToken: string): Promise<GraphQLUserInfo> {
async function fetchUserInfo(email: string, apiToken: string): Promise<GraphQLUserInfo> {
// Use api.atlassian.com as the endpoint for authentication
// This works regardless of the user's specific site
try {
const response = await fetch(`https://${host}/gateway/api/graphql`, {
const response = await fetch(`https://api.atlassian.com/graphql`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand Down Expand Up @@ -127,27 +110,3 @@ async function fetchUserInfo(host: string, email: string, apiToken: string): Pro
throw new Error('Network error occurred while validating credentials. Please check your connection.');
}
}

/**
* Fetches the cloud ID for the given Atlassian Cloud host.
*/
async function fetchCloudId(host: string): Promise<string> {
try {
const response = await fetch(`https://${host}/_edge/tenant_info`);
if (!response.ok) {
RovoDevLogger.error(new Error(`HTTP ${response.status}`), 'Failed to fetch cloud ID');
throw new Error('Failed to retrieve site information. Please try again.');
}
const data = await response.json();
if (!data.cloudId) {
throw new Error('Site information does not contain cloud ID.');
}
return data.cloudId;
} catch (error) {
if (error instanceof Error && error.message.includes('Site information')) {
throw error;
}
RovoDevLogger.error(error, 'Error fetching cloud ID');
throw new Error('Failed to retrieve site information. Please check your connection and try again.');
}
}
2 changes: 0 additions & 2 deletions src/rovo-dev/rovoDevLanguageServerProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@ describe('RovoDevLanguageServerProvider', () => {
// Default to Started state so LSP can start
mockCurrentState = {
state: 'Started',
jiraSiteHostname: 'test.atlassian.net',
jiraSiteUserInfo: { id: '123', displayName: 'Test User', email: 'test@example.com', avatarUrl: '' },
pid: 12345,
hostname: '127.0.0.1',
Expand Down Expand Up @@ -199,7 +198,6 @@ describe('RovoDevLanguageServerProvider', () => {
// Simulate state change to Started
setMockProcessState({
state: 'Started',
jiraSiteHostname: 'test.atlassian.net',
jiraSiteUserInfo: { id: '123', displayName: 'Test User', email: 'test@example.com', avatarUrl: '' },
pid: 12345,
hostname: '127.0.0.1',
Expand Down
Loading
Loading