Skip to content
Merged
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
61 changes: 61 additions & 0 deletions frontend/__tests__/utils/authErrorHandler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { describe, it, expect } from 'vitest';
import { isAuthenticationExpiredError, getAuthErrorMessage, shouldRedirectToLogin } from '../../utils/authErrorHandler';

describe('authErrorHandler', () => {
describe('isAuthenticationExpiredError', () => {
it('should return true for InteractionRequiredAuthError-like objects', () => {
const error = {
name: 'InteractionRequiredAuthError',
message: 'interaction_required: AADSTS160021: Application requested a user session which does not exist'
};
expect(isAuthenticationExpiredError(error)).toBe(true);
});

it('should return true for messages containing AADSTS160021', () => {
const error = new Error('interaction_required: AADSTS160021: Application requested a user session which does not exist');
expect(isAuthenticationExpiredError(error)).toBe(true);
});

it('should return true for session expired patterns', () => {
const error = new Error('user session which does not exist');
expect(isAuthenticationExpiredError(error)).toBe(true);
});

it('should return false for other errors', () => {
const error = new Error('Network error');
expect(isAuthenticationExpiredError(error)).toBe(false);
});
});

describe('getAuthErrorMessage', () => {
it('should return session expired message for authentication errors', () => {
const error = new Error('AADSTS160021: Application requested a user session which does not exist');
const message = getAuthErrorMessage(error);
expect(message).toBe('Your session has expired. Please sign out and sign in again to continue.');
});

it('should return permission message for 403 errors', () => {
const error = new Error('403 Forbidden');
const message = getAuthErrorMessage(error);
expect(message).toBe("You don't have permission to access this resource. Please check your permissions or contact your administrator.");
});

it('should return default auth error message for other errors', () => {
const error = new Error('Some other error');
const message = getAuthErrorMessage(error);
expect(message).toBe('Some other error');
});
});

describe('shouldRedirectToLogin', () => {
it('should return true for authentication expired errors', () => {
const error = new Error('AADSTS160021: Application requested a user session which does not exist');
expect(shouldRedirectToLogin(error)).toBe(true);
});

it('should return false for other errors', () => {
const error = new Error('Network error');
expect(shouldRedirectToLogin(error)).toBe(false);
});
});
});
19 changes: 15 additions & 4 deletions frontend/pages/QueryGeneratorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getSavedQueries, saveQuery, updateSavedQuery, deleteSavedQuery } from '
import { generateIpynbContent, downloadFile } from '../services/notebookService';
import { QueryResultData, DbInfo, CollectionInfo, CosmosDBAccount, SelectedResource, DebuggingResult, AnalysisResult, NotebookStep, SavedQuery } from '../types';
import { mockECommerceDbInfo, mockCollectionInfoMap, mockFindUsersQuery, mockUserFindResult, mockSavedQueries } from '../services/mockData';
import { getAuthErrorMessage, isAuthenticationExpiredError } from '../utils/authErrorHandler';
import QueryDisplay from '../components/QueryDisplay';
import QueryResult from '../components/QueryResult';
import Loader from '../components/Loader';
Expand Down Expand Up @@ -431,7 +432,10 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
setAzureAccounts(accounts);
} catch (e) {
if (e instanceof Error) {
if (e.message.includes('AuthorizationFailed') || e.message.includes('403')) {
// Check for authentication-related errors
if (isAuthenticationExpiredError(e)) {
setDbError(getAuthErrorMessage(e));
} else if (e.message.includes('AuthorizationFailed') || e.message.includes('403')) {
setDbError("Permission Denied: You may not have the required permissions to list Azure resources. Please contact your administrator.");
} else {
setDbError("Could not load Azure accounts from server. Ensure the backend is running and you have permissions.");
Expand All @@ -450,8 +454,12 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
const queries = await getSavedQueries();
setSavedQueries(queries);
} catch(e) {
// Handle error silently in the UI for now
console.error("Failed to fetch saved queries:", e);
// Log error details for debugging
if (e instanceof Error && isAuthenticationExpiredError(e)) {
console.error("Failed to fetch saved queries due to authentication error:", getAuthErrorMessage(e));
} else {
console.error("Failed to fetch saved queries:", e);
}
} finally {
setIsLoadingSavedQueries(false);
}
Expand Down Expand Up @@ -552,7 +560,10 @@ const QueryGeneratorPage: React.FC<QueryGeneratorPageProps> = ({ name, email, on
setAccountDatabases(dbs);
} catch(e) {
if (e instanceof Error) {
if (e.message.includes('AuthorizationFailed') || e.message.includes('403')) {
// Check for authentication-related errors first
if (isAuthenticationExpiredError(e)) {
setDbError(getAuthErrorMessage(e));
} else if (e.message.includes('AuthorizationFailed') || e.message.includes('403')) {
setDbError("Permission Denied: You may not have the required Azure role (e.g., 'Cosmos DB Operator') to access databases for this account. Please check your permissions.");
} else {
setDbError(e.message);
Expand Down
125 changes: 70 additions & 55 deletions frontend/services/dbService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export async function deleteDocument(collectionName: string, resource: SelectedR
import { DbInfo, CollectionInfo, CosmosDBAccount, SelectedResource, PaginatedDocumentsResponse, FoundDocumentResponse, DocumentHistoryResponse } from '../types';
import { msalInstance, loginRequest } from '../authConfig';
import { USE_MSAL_AUTH, API_BASE_URL } from '../app.config';
import { getAuthErrorMessage, isAuthenticationExpiredError } from '../utils/authErrorHandler';
import {
mockCosmosAccounts,
mockDatabasesByAccountId,
Expand All @@ -60,6 +61,32 @@ import {
mockUpdateDocument
} from './mockData';

/**
* Helper function to get access token with proper error handling
*/
const getAuthenticatedToken = async (): Promise<string> => {
try {
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
throw new Error("No signed-in user found.");
}

const response = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});

return response.accessToken;
} catch (error) {
// Handle authentication errors with user-friendly messages
if (isAuthenticationExpiredError(error)) {
throw new Error(getAuthErrorMessage(error));
}
// Re-throw other errors as-is
throw error;
}
};

/**
* Fetches available Azure Cosmos DB resources from the backend.
* @returns A promise that resolves with an array of Cosmos DB resources.
Expand All @@ -73,34 +100,43 @@ export const getAzureCosmosAccounts = async (): Promise<CosmosDBAccount[]> => {
}
// --- END DEVELOPMENT MOCK ---

const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
throw new Error("No signed-in user found.");
}
try {
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
throw new Error("No signed-in user found.");
}

// acquire token for backend API (must be set in loginRequest.scopes)
const response = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});
// acquire token for backend API (must be set in loginRequest.scopes)
const response = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});

const accessToken = response.accessToken;

console.log("Fetching Azure cosmosdb accounts from backend...");
const responseApi = await fetch(`${API_BASE_URL}/azure/cosmos_accounts`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});
const accessToken = response.accessToken;
console.log("Fetching Azure cosmosdb accounts from backend...");
const responseApi = await fetch(`${API_BASE_URL}/azure/cosmos_accounts`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});

if (!responseApi.ok) {
const errorData = await responseApi.json().catch(() => ({}));
const errorMessage = errorData.detail || errorData.message || `Could not load Azure resource list from server. Status: ${responseApi.status}`;
throw new Error(errorMessage);
if (!responseApi.ok) {
const errorData = await responseApi.json().catch(() => ({}));
const errorMessage = errorData.detail || errorData.message || `Could not load Azure resource list from server. Status: ${responseApi.status}`;
throw new Error(errorMessage);
}

return responseApi.json();
} catch (error) {
// Handle authentication errors with user-friendly messages
if (isAuthenticationExpiredError(error)) {
throw new Error(getAuthErrorMessage(error));
}
// Re-throw other errors as-is
throw error;
}

return responseApi.json();
};


Expand Down Expand Up @@ -129,18 +165,9 @@ export const getDatabasesForAccount = async (accountId: string): Promise<DbInfo[
// --- END DEVELOPMENT MOCK ---

console.log(`Fetching databases for account ID ${accountId} from backend...`);
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
throw new Error("No signed-in user found.");
}

// acquire token for backend API (must be set in loginRequest.scopes)
const tokenResponse = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});

const accessToken = tokenResponse.accessToken;

// Use helper function to get authenticated token with proper error handling
const accessToken = await getAuthenticatedToken();

const response = await fetch(`${API_BASE_URL}/azure/account_details`, {
method: 'POST',
Expand Down Expand Up @@ -179,18 +206,9 @@ export const getCollectionInfo = async (collectionName: string, resource: Select
return Promise.reject(new Error(`Mock collection info not found for ${collectionName}`));
}
// --- END DEVELOPMENT MOCK ---
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
throw new Error("No signed-in user found.");
}

// acquire token for backend API (must be set in loginRequest.scopes)
const tokenResponse = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});

const accessToken = tokenResponse.accessToken;

// Use helper function to get authenticated token with proper error handling
const accessToken = await getAuthenticatedToken();

console.log(`Fetching info for collection: ${collectionName} from backend...`);
const response = await fetch(`${API_BASE_URL}/azure/collection_info`, {
Expand Down Expand Up @@ -242,13 +260,10 @@ export const runMongoQuery = async (accountId: string, query: string, resource:
}
// --- END DEVELOPMENT MOCK ---
console.log(`Fetching databases for account ID ${accountId} from backend...`);
const accounts = msalInstance.getAllAccounts();
const tokenResponse = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});

const accessToken = tokenResponse.accessToken;

// Use helper function to get authenticated token with proper error handling
const accessToken = await getAuthenticatedToken();

console.log(`Sending query for execution on ${resource.databaseName} to backend...`);
const response = await fetch(`${API_BASE_URL}/query/execute`, {
method: 'POST',
Expand Down
26 changes: 18 additions & 8 deletions frontend/services/userDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,27 @@ import { SavedQuery } from '../types';
import { msalInstance, loginRequest } from '../authConfig';
import { USE_MSAL_AUTH, API_BASE_URL } from '../app.config';
import { mockDelay, mockSavedQueries } from './mockData';
import { getAuthErrorMessage, isAuthenticationExpiredError } from '../utils/authErrorHandler';

const getAccessToken = async (): Promise<string> => {
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
throw new Error("No signed-in user found.");
try {
const accounts = msalInstance.getAllAccounts();
if (accounts.length === 0) {
throw new Error("No signed-in user found.");
}
const response = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});
return response.accessToken;
} catch (error) {
// Handle authentication errors with user-friendly messages
if (isAuthenticationExpiredError(error)) {
throw new Error(getAuthErrorMessage(error));
}
// Re-throw other errors as-is
throw error;
}
const response = await msalInstance.acquireTokenSilent({
...loginRequest,
account: accounts[0],
});
return response.accessToken;
};

/**
Expand Down
Loading