From 26a77bfb6b5b18d6c0e8b98e1e4c4d938ccb951c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 01:33:00 +0000 Subject: [PATCH 1/3] Initial plan From 352b7ecaf5bd37920365cfdfdd07841c19cea5fe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 01:40:33 +0000 Subject: [PATCH 2/3] Fix organization loading error and improve PAT documentation - Handle missing read:org permission gracefully in getUserOrganizations() - Return empty array instead of throwing when org permission is missing - Update error message to guide users on missing permission - Clarify that read:org/Members permission is optional in documentation - Update README, PATSetupInstructions, and PATLogin error messages Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com> --- README.md | 5 ++++- src/components/OrganizationSelection.js | 12 +++++++++++- src/components/PATLogin.js | 2 +- src/components/PATSetupInstructions.css | 18 ++++++++++++++++++ src/components/PATSetupInstructions.js | 6 +++++- src/services/githubService.js | 9 +++++++++ 6 files changed, 48 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c226f5a466..6cd7707394 100644 --- a/README.md +++ b/README.md @@ -180,10 +180,13 @@ For **fine-grained tokens**: - **Contents**: Read and Write (for editing DAK content) - **Metadata**: Read (for repository information) - **Pull requests**: Read and Write (for creating pull requests) +- **Members**: Read-only (optional - only needed if you want to list organizations you're a member of) For **classic tokens**: - **repo**: Full control of private repositories (for editing DAK content) -- **read:org**: Read org and team membership (for listing organization repositories) +- **read:org**: Read org and team membership (optional - only needed if you want to list organizations you're a member of) + +**Note**: The `read:org` scope (classic) or `Members: Read-only` permission (fine-grained) is only needed if you want to see and select organizations you're a member of. You can still use your personal account and the WHO organization without this permission. This authentication method is fully compatible with static deployments and requires no backend server. diff --git a/src/components/OrganizationSelection.js b/src/components/OrganizationSelection.js index 2e3055fb0c..b48016cc77 100644 --- a/src/components/OrganizationSelection.js +++ b/src/components/OrganizationSelection.js @@ -103,7 +103,17 @@ const OrganizationSelection = () => { setOrganizations(orgsData); } catch (error) { console.error('Error fetching organizations:', error); - setError('Failed to fetch organizations. Please check your connection and try again.'); + + // Provide helpful error message based on error type + let errorMessage = 'Failed to fetch organizations. '; + if (error.status === 403 || error.status === 401) { + errorMessage = 'Unable to list your organizations. Your Personal Access Token needs the "read:org" scope (classic tokens) or "Members: Read-only" permission (fine-grained tokens) to list organizations. You can still use your personal account or select WHO.'; + } else { + errorMessage += 'Please check your connection and try again.'; + } + + setError(errorMessage); + // Fallback to mock data for demonstration (which includes WHO) try { const mockOrgs = await getMockOrganizations(); diff --git a/src/components/PATLogin.js b/src/components/PATLogin.js index 1c6f9cabc2..23aadc2e15 100644 --- a/src/components/PATLogin.js +++ b/src/components/PATLogin.js @@ -63,7 +63,7 @@ const PATLogin = ({ onAuthSuccess }) => { if (err.status === 401) { setError("Invalid Personal Access Token. Please check your token and try again."); } else if (err.status === 403) { - setError("Token doesn't have sufficient permissions. Please ensure your token has 'repo' and 'read:org' scopes."); + setError("Token doesn't have sufficient permissions. Please ensure your token has 'repo' scope (classic) or 'Contents: Read and Write' + 'Pull requests: Read and Write' (fine-grained)."); } else { setError("Authentication failed. Please check your connection and try again."); } diff --git a/src/components/PATSetupInstructions.css b/src/components/PATSetupInstructions.css index a62a909b8d..997c2b9c60 100644 --- a/src/components/PATSetupInstructions.css +++ b/src/components/PATSetupInstructions.css @@ -78,6 +78,24 @@ color: #856404; } +.info-note { + background: #e6f3ff; + border: 1px solid #b3d9ff; + border-radius: 4px; + padding: 0.75rem; + margin: 0.5rem 0; + font-size: 0.9rem; + color: #004085; +} + +.info-note code { + background: rgba(0, 64, 133, 0.1); + padding: 0.2em 0.4em; + border-radius: 3px; + font-family: 'Courier New', Courier, monospace; + font-size: 0.85em; +} + .benefits-section, .security-note { background: white; diff --git a/src/components/PATSetupInstructions.js b/src/components/PATSetupInstructions.js index 8261e6e92e..8db128d27a 100644 --- a/src/components/PATSetupInstructions.js +++ b/src/components/PATSetupInstructions.js @@ -32,12 +32,16 @@ const PATSetupInstructions = () => {
  • Contents: Read and Write
  • Metadata: Read
  • Pull requests: Read and Write
  • +
  • Members: Read-only (optional - only if you want to list organizations)
  • For classic tokens, select these scopes:

    +
    + ℹ️ Note: The organization permission (read:org for classic or Members: Read-only for fine-grained) is only needed if you want to see organizations you're a member of. You can still use your personal account and the WHO organization without it. +
    diff --git a/src/services/githubService.js b/src/services/githubService.js index e249d0fa39..31f3d7fe07 100644 --- a/src/services/githubService.js +++ b/src/services/githubService.js @@ -350,6 +350,15 @@ class GitHubService { return data; } catch (error) { console.error('Failed to fetch organizations:', error); + + // Handle permission errors gracefully + if (error.status === 403 || error.status === 401) { + console.warn('Token does not have permission to list organizations. This requires the "read:org" scope for classic tokens or "Members: Read" for fine-grained tokens.'); + // Return empty array instead of throwing - user can still use their personal account + return []; + } + + // For other errors (network issues, etc.), throw so the UI can show retry option throw error; } } From 8930f2ebf3c3b68f41f77332d2dccdd71002015d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Oct 2025 01:45:29 +0000 Subject: [PATCH 3/3] Add tests for getUserOrganizations permission handling - Test successful organization retrieval with proper permissions - Test graceful handling of 403/401 permission errors (returns empty array) - Test error throwing for non-permission errors (network issues) - Test authentication requirement - All 5 new tests passing Co-authored-by: litlfred <662242+litlfred@users.noreply.github.com> --- src/services/githubService.test.js | 80 ++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/services/githubService.test.js b/src/services/githubService.test.js index 896faa2005..e392efbf04 100644 --- a/src/services/githubService.test.js +++ b/src/services/githubService.test.js @@ -636,4 +636,84 @@ describe('GitHubService', () => { expect(result).toBeNull(); }); }); + + describe('getUserOrganizations', () => { + beforeEach(() => { + mockOctokit.rest.orgs = { + listForAuthenticatedUser: jest.fn() + }; + }); + + it('should return organizations when authenticated with proper permissions', async () => { + // Manually set authentication state + githubService.octokit = mockOctokit; + githubService.isAuthenticated = true; + + const mockOrgs = [ + { id: 1, login: 'my-org', name: 'My Organization' }, + { id: 2, login: 'another-org', name: 'Another Org' } + ]; + + mockOctokit.rest.orgs.listForAuthenticatedUser.mockResolvedValue({ + status: 200, + data: mockOrgs + }); + + const result = await githubService.getUserOrganizations(); + + expect(result).toEqual(mockOrgs); + expect(mockOctokit.rest.orgs.listForAuthenticatedUser).toHaveBeenCalled(); + }); + + it('should return empty array when token lacks read:org permission (403)', async () => { + // Manually set authentication state + githubService.octokit = mockOctokit; + githubService.isAuthenticated = true; + + const permissionError = new Error('Resource not accessible by personal access token'); + permissionError.status = 403; + + mockOctokit.rest.orgs.listForAuthenticatedUser.mockRejectedValue(permissionError); + + const result = await githubService.getUserOrganizations(); + + expect(result).toEqual([]); + expect(mockOctokit.rest.orgs.listForAuthenticatedUser).toHaveBeenCalled(); + }); + + it('should return empty array when token lacks read:org permission (401)', async () => { + // Manually set authentication state + githubService.octokit = mockOctokit; + githubService.isAuthenticated = true; + + const permissionError = new Error('Requires authentication'); + permissionError.status = 401; + + mockOctokit.rest.orgs.listForAuthenticatedUser.mockRejectedValue(permissionError); + + const result = await githubService.getUserOrganizations(); + + expect(result).toEqual([]); + expect(mockOctokit.rest.orgs.listForAuthenticatedUser).toHaveBeenCalled(); + }); + + it('should throw error for non-permission errors', async () => { + // Manually set authentication state + githubService.octokit = mockOctokit; + githubService.isAuthenticated = true; + + const networkError = new Error('Network error'); + networkError.status = 500; + + mockOctokit.rest.orgs.listForAuthenticatedUser.mockRejectedValue(networkError); + + await expect(githubService.getUserOrganizations()).rejects.toThrow('Network error'); + }); + + it('should throw error when not authenticated', async () => { + githubService.logout(); + + await expect(githubService.getUserOrganizations()).rejects.toThrow('Not authenticated with GitHub'); + }); + }); }); \ No newline at end of file