Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: login to gitlab from native app #822

Merged
merged 10 commits into from
Jun 9, 2022
Merged
2 changes: 1 addition & 1 deletion android/app/capacitor.build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ android {

apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {

implementation project(':capacitor-app')

}

Expand Down
7 changes: 7 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="organice.200ok.ch" />
</intent-filter>

</activity>

<provider
Expand Down
3 changes: 2 additions & 1 deletion android/app/src/main/assets/capacitor.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"appId": "ch.twohundredok.organice",
"appName": "organice",
"webDir": "build",
"bundledWebRuntime": false
"bundledWebRuntime": false,
"server": {}
}
7 changes: 6 additions & 1 deletion android/app/src/main/assets/capacitor.plugins.json
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
[]
[
{
"pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
}
]
3 changes: 3 additions & 0 deletions android/capacitor.settings.gradle
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
8 changes: 7 additions & 1 deletion capacitor.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ const config: CapacitorConfig = {
appId: 'ch.twohundredok.organice',
appName: 'organice',
webDir: 'build',
bundledWebRuntime: false
bundledWebRuntime: false,
server: {
// hostname: 'localhost', // default: localhost
// iosScheme: 'organice', // default: ionic
// androidScheme: 'organice', // default: http
// allowNavigation: ['https://gitlab.com']
}
};

export default config;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"@babel/helper-environment-visitor": "^7.18.2",
"@bity/oauth2-auth-code-pkce": "^2.13.0",
"@capacitor/android": "^3.5.1",
"@capacitor/app": "^1.1.1",
"@capacitor/core": "^3.5.1",
"bowser": "^2.11.0",
"classnames": "^2.2.6",
Expand Down
124 changes: 67 additions & 57 deletions src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,22 +40,26 @@ import {
import _ from 'lodash';
import { Map } from 'immutable';

import AppUrlListener from './AppUrlListener';

import { configure } from 'react-hotkeys';
// do handle hotkeys even if they come from within 'input', 'select' or 'textarea'
configure({ ignoreTags: [] });

const handleGitLabAuthResponse = async (oauthClient) => {
let success = false;
let error;
try {
success = await oauthClient.isReturningFromAuthServer();
await oauthClient.getAccessToken();
} catch {
} catch (e) {
error = e;
success = false;
}
if (!success) {
// Edge case: somehow OAuth success redirect occurred but there isn't a code in
// the current location's search params. This /shouldn't/ happen in practice.
alert('Unexpected sign in error, please try again');
alert('Unexpected sign in error, please try again: ' + error);
return;
}

Expand All @@ -68,75 +72,80 @@ const handleGitLabAuthResponse = async (oauthClient) => {
}
};

export default class App extends PureComponent {
constructor(props) {
super(props);

runAllMigrations();

const initialState = readInitialState();

window.initialHash = window.location.hash.substring(0);
const hashContents = parseQueryString(window.location.hash);
const authenticatedSyncService = getPersistedField('authenticatedSyncService', true);
let client = null;

if (!!authenticatedSyncService) {
switch (authenticatedSyncService) {
case 'Dropbox':
const dropboxAccessToken = hashContents.access_token;
if (dropboxAccessToken) {
client = createDropboxSyncBackendClient(dropboxAccessToken);
initialState.syncBackend = Map({
isAuthenticated: true,
client,
});
persistField('dropboxAccessToken', dropboxAccessToken);
window.location.hash = '';
} else {
const persistedDropboxAccessToken = getPersistedField('dropboxAccessToken', true);
if (!!persistedDropboxAccessToken) {
client = createDropboxSyncBackendClient(persistedDropboxAccessToken);
initialState.syncBackend = Map({
isAuthenticated: true,
client,
});
}
}
break;
case 'Google Drive':
client = createGoogleDriveSyncBackendClient();
export function handleAuthenticatedSyncService(initialState) {
window.initialHash = window.location.hash.substring(0);
const hashContents = parseQueryString(window.location.hash);
const authenticatedSyncService = getPersistedField('authenticatedSyncService', true);
let client = null;

if (!!authenticatedSyncService) {
switch (authenticatedSyncService) {
case 'Dropbox':
const dropboxAccessToken = hashContents.access_token;
if (dropboxAccessToken) {
client = createDropboxSyncBackendClient(dropboxAccessToken);
initialState.syncBackend = Map({
isAuthenticated: true,
client,
});
break;
case 'GitLab':
const gitlabOAuth = createGitlabOAuth();
if (gitlabOAuth.isAuthorized()) {
client = createGitLabSyncBackendClient(gitlabOAuth);
persistField('dropboxAccessToken', dropboxAccessToken);
window.location.hash = '';
} else {
const persistedDropboxAccessToken = getPersistedField('dropboxAccessToken', true);
if (!!persistedDropboxAccessToken) {
client = createDropboxSyncBackendClient(persistedDropboxAccessToken);
initialState.syncBackend = Map({
isAuthenticated: true,
client,
});
} else {
handleGitLabAuthResponse(gitlabOAuth);
}
break;
case 'WebDAV':
client = createWebDAVSyncBackendClient(
getPersistedField('webdavEndpoint'),
getPersistedField('webdavUsername'),
getPersistedField('webdavPassword')
);
}
break;
case 'Google Drive':
client = createGoogleDriveSyncBackendClient();
initialState.syncBackend = Map({
isAuthenticated: true,
client,
});
break;
case 'GitLab':
const gitlabOAuth = createGitlabOAuth();
if (gitlabOAuth.isAuthorized()) {
client = createGitLabSyncBackendClient(gitlabOAuth);
initialState.syncBackend = Map({
isAuthenticated: true,
client,
});
break;
default:
}
} else {
handleGitLabAuthResponse(gitlabOAuth);
}
break;
case 'WebDAV':
client = createWebDAVSyncBackendClient(
getPersistedField('webdavEndpoint'),
getPersistedField('webdavUsername'),
getPersistedField('webdavPassword')
);
initialState.syncBackend = Map({
isAuthenticated: true,
client,
});
break;
default:
}
}
return client;
}

export default class App extends PureComponent {
constructor(props) {
super(props);

runAllMigrations();

const initialState = readInitialState();

const client = handleAuthenticatedSyncService(initialState);

const queryStringContents = parseQueryString(window.location.search);
const { captureFile, captureTemplateName, captureContent } = queryStringContents;
Expand Down Expand Up @@ -217,6 +226,7 @@ export default class App extends PureComponent {
return (
<DragDropContext onDragEnd={this.handleDragEnd}>
<Router>
<AppUrlListener></AppUrlListener>
<Provider store={this.store}>
<div className="App">
<Entry />
Expand Down
46 changes: 46 additions & 0 deletions src/AppUrlListener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { useEffect } from 'react';
// import { useHistory } from 'react-router-dom';
import { App } from '@capacitor/app';
import { handleAuthenticatedSyncService } from './App';
import { readInitialState } from './util/settings_persister';

const AppUrlListener = () => {
// let history = useHistory();
useEffect(() => {
App.addListener('appUrlOpen', (event) => {
// Some OAuth providers (i.e. GitLab) use query parameters.
// Migrate them from the event.url to the actual
// window.location.
const newUrl = new URL(window.location.href);
for (const entry of new URL(event.url).searchParams.entries()) {
newUrl.searchParams.set(entry[0], entry[1]);
}
// Some OAuth providers (i.e. Dropbox) use the hash. Migrate it
// from the event.url to window.location.
newUrl.hash = new URL(event.url).hash;
window.history.pushState(null, '', newUrl);
handleAuthenticatedSyncService(readInitialState());

// HACK: After the above code, the Dropbox login has succeeded.
// However, the constructor in App.js has to run. For some
// reason, this implicitly happens for GitLab. This could most
// definitively be implemented in a more readable manner by
// refactoring the App.js constructor.
if (newUrl.hash) window.location.reload();

// // Example url: https://beerswift.app/tabs/tab2
// // slug = /tabs/tab2
// const slug = event.url.split('organice.200ok.ch').pop();
// if (slug) {
// //alert(slug);
// history.push(slug);
// }
// // If no match, do nothing - let regular routing
// // logic take over
});
}, []);

return null;
};

export default AppUrlListener;
4 changes: 3 additions & 1 deletion src/components/SyncServiceSignIn/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
import { Dropbox } from 'dropbox';
import _ from 'lodash';

import { redirectUrl } from '../../util/redirect_url';

function WebDAVForm() {
const [isVisible, setIsVisible] = useState(false);
const toggleVisible = () => setIsVisible(!isVisible);
Expand Down Expand Up @@ -169,7 +171,7 @@ export default class SyncServiceSignIn extends PureComponent {
clientId: process.env.REACT_APP_DROPBOX_CLIENT_ID,
fetch: fetch.bind(window),
});
dropbox.auth.getAuthenticationUrl(window.location.origin + '/').then((authURL) => {
dropbox.auth.getAuthenticationUrl(redirectUrl()).then((authURL) => {
window.location = authURL;
});
}
Expand Down
3 changes: 2 additions & 1 deletion src/sync_backend_clients/gitlab_sync_backend_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { OAuth2AuthCodePKCE } from '@bity/oauth2-auth-code-pkce';
import { orgFileExtensions } from '../lib/org_utils';
import { getPersistedField } from '../util/settings_persister';
import { redirectUrl } from '../util/redirect_url';

import { fromJS, Map } from 'immutable';

Expand All @@ -15,7 +16,7 @@ export const createGitlabOAuth = () => {
authorizationUrl: 'https://gitlab.com/oauth/authorize',
tokenUrl: 'https://gitlab.com/oauth/token',
clientId: process.env.REACT_APP_GITLAB_CLIENT_ID,
redirectUrl: window.location.origin,
redirectUrl: redirectUrl(),
scopes: ['api'],
extraAuthorizationParams: {
clientSecret: process.env.REACT_APP_GITLAB_SECRET,
Expand Down
8 changes: 8 additions & 0 deletions src/util/redirect_url.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Capacitor } from '@capacitor/core';

export const redirectUrl = () => {
// https://capacitorjs.com/docs/core-apis/web#isnativeplatform
if (Capacitor.isNativePlatform()) return 'https://organice.200ok.ch/';

return window.location.origin + '/';
};
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,11 @@
resolved "https://registry.yarnpkg.com/@capacitor/android/-/android-3.5.1.tgz#d81ec82f84ab7f0abeadb4cc100e6a3820197d65"
integrity sha512-rjehS0+BQBlwoN8hUyrMuzexn/9QJsONb1kmN5uXcL8JuTEbv35fa7z0tSD4x1LKwUFd+3Zeuwt60QRuwijlmw==

"@capacitor/app@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@capacitor/app/-/app-1.1.1.tgz#bde7202faad1b47c4ae2c4a622285d7569384c5e"
integrity sha512-8ADkldHnoE1xkWvPUsGlERVGm6/Zvcxy6hCI80AxydIKyaCG7kbDAvUclebbnw/eFRxj2zBoVatGLjmJNvTbYw==

"@capacitor/cli@^3.5.1":
version "3.5.1"
resolved "https://registry.yarnpkg.com/@capacitor/cli/-/cli-3.5.1.tgz#7184554f3460a6fa0988ed7db2617fd53e7eade5"
Expand Down