diff --git a/src/App.js b/src/App.js index b3de033b0..dcb7ba8f9 100644 --- a/src/App.js +++ b/src/App.js @@ -88,7 +88,8 @@ export default class App extends PureComponent { }); break; case 'GitLab': - const gitlabOAuth = createGitlabOAuth(); + let project = getPersistedField('gitLabURL'); + const gitlabOAuth = createGitlabOAuth(project); if (gitlabOAuth.isAuthorized()) { client = createGitLabSyncBackendClient(gitlabOAuth); initialState.syncBackend = Map({ diff --git a/src/components/SyncServiceSignIn/index.js b/src/components/SyncServiceSignIn/index.js index 487466a94..ee80d6a5c 100644 --- a/src/components/SyncServiceSignIn/index.js +++ b/src/components/SyncServiceSignIn/index.js @@ -10,7 +10,6 @@ import GitLabLogo from './gitlab.svg'; import { persistField } from '../../util/settings_persister'; import { createGitlabOAuth, - gitLabProjectIdFromURL, } from '../../sync_backend_clients/gitlab_sync_backend_client'; import { DropboxAuth } from 'dropbox'; @@ -113,15 +112,10 @@ function GitLab() { const defaultProject = 'https://gitlab.com/your/project'; const [project, setProject] = useState(defaultProject); const handleSubmit = (evt) => { - const projectId = gitLabProjectIdFromURL(project); - if (projectId) { - persistField('authenticatedSyncService', 'GitLab'); - persistField('gitLabProject', projectId); - createGitlabOAuth().fetchAuthorizationCode(); - } else { - evt.preventDefault(); - alert('Project does not appear to be a valid gitlab.com URL'); - } + persistField('authenticatedSyncService', 'GitLab'); + persistField('gitLabURL', project); + // TODO handle incorrect URLs, possibly with try ... catch ... + createGitlabOAuth(project).fetchAuthorizationCode(); }; return ( diff --git a/src/sync_backend_clients/gitlab_sync_backend_client.js b/src/sync_backend_clients/gitlab_sync_backend_client.js index 92fefc7ee..b50a13f3b 100644 --- a/src/sync_backend_clients/gitlab_sync_backend_client.js +++ b/src/sync_backend_clients/gitlab_sync_backend_client.js @@ -5,15 +5,16 @@ import { getPersistedField } from '../util/settings_persister'; import { fromJS, Map } from 'immutable'; -export const createGitlabOAuth = () => { +export const createGitlabOAuth = (url = 'https://gitlab.com') => { // Use promises as mutex to prevent concurrent token refresh attempts, which causes problems. // More info: https://github.com/BitySA/oauth2-auth-code-pkce/issues/29 // TODO: remove this workaround if/when oauth2-auth-code-pkce fixes the issue. let expiryPromise; let invalidGrantPromise; + url = new URL(url) return new OAuth2AuthCodePKCE({ - authorizationUrl: 'https://gitlab.com/oauth/authorize', - tokenUrl: 'https://gitlab.com/oauth/token', + authorizationUrl: url.origin + '/oauth/authorize', + tokenUrl: url.origin + '/oauth/token', clientId: process.env.REACT_APP_GITLAB_CLIENT_ID, redirectUrl: window.location.origin, scopes: ['api'], @@ -64,7 +65,7 @@ export const gitLabProjectIdFromURL = (projectURL) => { // to a project. Reminder: a project path is not necessarily // /user/project because it may be under one or more groups such // as /user/group/subgroup/project. - if (url.hostname === 'gitlab.com' && path.split('/').length > 1) { + if (path.split('/').length > 1) { return encodeURIComponent(path); } else { return undefined; @@ -130,7 +131,7 @@ export const treeToDirectoryListing = (tree) => { ); }; -const API_URL = 'https://gitlab.com/api/v4'; +const API_PATH = '/api/v4/' /** * GitLab sync backend, implemented using their REST API. @@ -141,7 +142,14 @@ const API_URL = 'https://gitlab.com/api/v4'; export default (oauthClient) => { const decoratedFetch = oauthClient.decorateFetchHTTPClient(fetch); - const getProjectApi = () => `${API_URL}/projects/${getPersistedField('gitLabProject')}`; + const getApi = () => { + let url = new URL(getPersistedField('gitLabURL')) + return url.origin + API_PATH + } + + const getProject = () => gitLabProjectIdFromURL(getPersistedField('gitLabURL')) + + const getProjectApi = () => getApi() + 'projects/' + getProject() const isSignedIn = async () => { if (!oauthClient.isAuthorized()) { @@ -174,7 +182,7 @@ export default (oauthClient) => { // commit. const [userResponse, membersResponse] = await Promise.all([ // https://docs.gitlab.com/ee/api/users.html#list-current-user-for-normal-users - decoratedFetch(`${API_URL}/user`), + decoratedFetch(getApi() + 'user'), // https://docs.gitlab.com/ee/api/members.html#list-all-members-of-a-group-or-project decoratedFetch(`${getProjectApi()}/members`), ]); diff --git a/src/sync_backend_clients/gitlab_sync_backend_client.test.js b/src/sync_backend_clients/gitlab_sync_backend_client.test.js index 24d285916..10192fd13 100644 --- a/src/sync_backend_clients/gitlab_sync_backend_client.test.js +++ b/src/sync_backend_clients/gitlab_sync_backend_client.test.js @@ -17,6 +17,21 @@ test('Parses GitLab project from URL', () => { }); }); +test('Parses GitLab project from URL of a self-managed GitLab instance', () => { + [ + ['https://mygitlab.com/user/foo', 'user%2Ffoo'], + ['https://mygitlab.com/group/subgroup/project', 'group%2Fsubgroup%2Fproject'], + ['mygitlab.com/foo/bar', 'foo%2Fbar'], + ['mygitlab.com/user-but-no-project', undefined], + ['https://www.mygitlab.com/user/foo', 'user%2Ffoo'], + ['https://www.mygitlab.com/group/subgroup/project', 'group%2Fsubgroup%2Fproject'], + ['www.mygitlab.com/foo/bar', 'foo%2Fbar'], + ['www.mygitlab.com/user-but-no-project', undefined], + ].forEach(([input, expected]) => { + expect(gitLabProjectIdFromURL(input)).toEqual(expected); + }); +}); + test('Parses Link pagination header', () => { [ [null, {}],