optionally connect in a popup
Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
This commit is contained in:
Родитель
bae3ff88a8
Коммит
782804e23f
|
@ -12,13 +12,13 @@ jobs:
|
|||
APP_ID: integration_github
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Use Node 14
|
||||
- name: Use Node 16
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 14
|
||||
node-version: 16
|
||||
|
||||
- name: Set up npm
|
||||
run: npm i -g npm
|
||||
run: npm i -g npm@^8.0.0
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
|
|
@ -10,14 +10,16 @@
|
|||
*/
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
['name' => 'config#oauthRedirect', 'url' => '/oauth-redirect', 'verb' => 'GET'],
|
||||
['name' => 'config#setConfig', 'url' => '/config', 'verb' => 'PUT'],
|
||||
['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'],
|
||||
['name' => 'githubAPI#getNotifications', 'url' => '/notifications', 'verb' => 'GET'],
|
||||
['name' => 'githubAPI#unsubscribeNotification', 'url' => '/notifications/{id}/unsubscribe', 'verb' => 'PUT'],
|
||||
['name' => 'githubAPI#markNotificationAsRead', 'url' => '/notifications/{id}/mark-read', 'verb' => 'PUT'],
|
||||
['name' => 'githubAPI#getAvatar', 'url' => '/avatar', 'verb' => 'GET'],
|
||||
'routes' => [
|
||||
['name' => 'config#oauthRedirect', 'url' => '/oauth-redirect', 'verb' => 'GET'],
|
||||
['name' => 'config#setConfig', 'url' => '/config', 'verb' => 'PUT'],
|
||||
['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'],
|
||||
['name' => 'config#popupSuccessPage', 'url' => '/popup-success', 'verb' => 'GET'],
|
||||
|
||||
['name' => 'githubAPI#getNotifications', 'url' => '/notifications', 'verb' => 'GET'],
|
||||
['name' => 'githubAPI#unsubscribeNotification', 'url' => '/notifications/{id}/unsubscribe', 'verb' => 'PUT'],
|
||||
['name' => 'githubAPI#markNotificationAsRead', 'url' => '/notifications/{id}/mark-read', 'verb' => 'PUT'],
|
||||
['name' => 'githubAPI#getAvatar', 'url' => '/avatar', 'verb' => 'GET'],
|
||||
// get user/issue/pr information
|
||||
['name' => 'githubAPI#getUserInfo', 'url' => '/users/{githubUserName}', 'verb' => 'GET'],
|
||||
['name' => 'githubAPI#getIssueInfo', 'url' => '/repos/{owner}/{repo}/issues/{issueNumber}', 'verb' => 'GET'],
|
||||
|
|
|
@ -11,6 +11,8 @@
|
|||
|
||||
namespace OCA\Github\Controller;
|
||||
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
|
@ -21,6 +23,7 @@ use OCP\AppFramework\Controller;
|
|||
|
||||
use OCA\Github\Service\GithubAPIService;
|
||||
use OCA\Github\AppInfo\Application;
|
||||
use OCP\PreConditionNotMetException;
|
||||
|
||||
class ConfigController extends Controller {
|
||||
|
||||
|
@ -44,12 +47,14 @@ class ConfigController extends Controller {
|
|||
* @var string|null
|
||||
*/
|
||||
private $userId;
|
||||
private IInitialState $initialStateService;
|
||||
|
||||
public function __construct(string $appName,
|
||||
IRequest $request,
|
||||
IConfig $config,
|
||||
IURLGenerator $urlGenerator,
|
||||
IL10N $l,
|
||||
IInitialState $initialStateService,
|
||||
GithubAPIService $githubAPIService,
|
||||
?string $userId) {
|
||||
parent::__construct($appName, $request);
|
||||
|
@ -58,6 +63,7 @@ class ConfigController extends Controller {
|
|||
$this->l = $l;
|
||||
$this->githubAPIService = $githubAPIService;
|
||||
$this->userId = $userId;
|
||||
$this->initialStateService = $initialStateService;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,6 +105,19 @@ class ConfigController extends Controller {
|
|||
return new DataResponse(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
*
|
||||
* @param string $user_name
|
||||
* @param string $user_displayname
|
||||
* @return TemplateResponse
|
||||
*/
|
||||
public function popupSuccessPage(string $user_name, string $user_displayname): TemplateResponse {
|
||||
$this->initialStateService->provideInitialState('popup-data', ['user_name' => $user_name, 'user_displayname' => $user_displayname]);
|
||||
return new TemplateResponse(Application::APP_ID, 'popupSuccess', [], TemplateResponse::RENDER_AS_GUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
* @NoCSRFRequired
|
||||
|
@ -108,6 +127,7 @@ class ConfigController extends Controller {
|
|||
* @param string $code request code to use when requesting oauth token
|
||||
* @param string $state value that was sent with original GET request. Used to check auth redirection is valid
|
||||
* @return RedirectResponse to user settings
|
||||
* @throws PreConditionNotMetException
|
||||
*/
|
||||
public function oauthRedirect(string $code, string $state): RedirectResponse {
|
||||
$configState = $this->config->getUserValue($this->userId, Application::APP_ID, 'oauth_state');
|
||||
|
@ -127,23 +147,34 @@ class ConfigController extends Controller {
|
|||
if (isset($result['access_token'])) {
|
||||
$accessToken = $result['access_token'];
|
||||
$this->config->setUserValue($this->userId, Application::APP_ID, 'token', $accessToken);
|
||||
$this->storeUserInfo($accessToken);
|
||||
$oauthOrigin = $this->config->getUserValue($this->userId, Application::APP_ID, 'oauth_origin');
|
||||
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'oauth_origin');
|
||||
if ($oauthOrigin === 'settings') {
|
||||
$userInfo = $this->storeUserInfo($accessToken);
|
||||
|
||||
$usePopup = $this->config->getAppValue(Application::APP_ID, 'use_popup', '0') === '1';
|
||||
if ($usePopup) {
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->linkToRoute('integration_github.config.popupSuccessPage', [
|
||||
'user_name' => $userInfo,
|
||||
'user_displayname' => $userInfo,
|
||||
])
|
||||
);
|
||||
} else {
|
||||
$oauthOrigin = $this->config->getUserValue($this->userId, Application::APP_ID, 'oauth_origin');
|
||||
$this->config->deleteUserValue($this->userId, Application::APP_ID, 'oauth_origin');
|
||||
if ($oauthOrigin === 'settings') {
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'connected-accounts']) .
|
||||
'?githubToken=success'
|
||||
);
|
||||
} elseif ($oauthOrigin === 'dashboard') {
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->linkToRoute('dashboard.dashboard.index')
|
||||
);
|
||||
}
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'connected-accounts']) .
|
||||
'?githubToken=success'
|
||||
);
|
||||
} elseif ($oauthOrigin === 'dashboard') {
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->linkToRoute('dashboard.dashboard.index')
|
||||
);
|
||||
}
|
||||
return new RedirectResponse(
|
||||
$this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'connected-accounts']) .
|
||||
'?githubToken=success'
|
||||
);
|
||||
}
|
||||
$result = $this->l->t('Error getting OAuth access token');
|
||||
} else {
|
||||
|
|
|
@ -98,10 +98,12 @@ class GithubWidget implements IWidget {
|
|||
$clientID = $this->config->getAppValue(Application::APP_ID, 'client_id');
|
||||
$clientSecret = $this->config->getAppValue(Application::APP_ID, 'client_secret');
|
||||
$oauthPossible = $clientID !== '' && $clientSecret !== '';
|
||||
$usePopup = $this->config->getAppValue(Application::APP_ID, 'use_popup', '0');
|
||||
|
||||
$userConfig = [
|
||||
'oauth_is_possible' => $oauthPossible,
|
||||
'client_id' => $clientID,
|
||||
'use_popup' => ($usePopup === '1'),
|
||||
];
|
||||
$this->initialStateService->provideInitialState('user-config', $userConfig);
|
||||
Util::addScript(Application::APP_ID, 'integration_github-dashboard');
|
||||
|
|
|
@ -43,11 +43,13 @@ class Personal implements ISettings {
|
|||
// for OAuth
|
||||
$clientID = $this->config->getAppValue(Application::APP_ID, 'client_id');
|
||||
$clientSecret = $this->config->getAppValue(Application::APP_ID, 'client_secret') !== '';
|
||||
$usePopup = $this->config->getAppValue(Application::APP_ID, 'use_popup', '0');
|
||||
|
||||
$userConfig = [
|
||||
'token' => $token,
|
||||
'client_id' => $clientID,
|
||||
'client_secret' => $clientSecret,
|
||||
'use_popup' => ($usePopup === '1'),
|
||||
'search_issues_enabled' => ($searchIssuesEnabled === '1'),
|
||||
'search_repos_enabled' => ($searchReposEnabled === '1'),
|
||||
'navigation_enabled' => ($navigationEnabled === '1'),
|
||||
|
|
|
@ -17,23 +17,24 @@
|
|||
@update:checked="onCheckboxChanged($event, 'navigation_enabled')">
|
||||
{{ t('integration_github', 'Enable navigation link') }}
|
||||
</CheckboxRadioSwitch>
|
||||
<div class="line">
|
||||
<label v-show="!showOAuth"
|
||||
for="github-token">
|
||||
<div v-show="!showOAuth"
|
||||
class="line">
|
||||
<label for="github-token">
|
||||
<KeyIcon :size="20" class="icon" />
|
||||
{{ t('integration_github', 'Personal access token') }}
|
||||
</label>
|
||||
<input v-show="!showOAuth"
|
||||
id="github-token"
|
||||
<input id="github-token"
|
||||
v-model="state.token"
|
||||
type="password"
|
||||
:disabled="connected === true"
|
||||
:placeholder="t('integration_github', 'GitHub personal access token')"
|
||||
@input="onInput"
|
||||
@keyup.enter="onConnectClick"
|
||||
@focus="readonly = false">
|
||||
</div>
|
||||
<NcButton v-if="showOAuth && !connected"
|
||||
@click="onOAuthClick">
|
||||
<NcButton v-if="!connected"
|
||||
:disabled="loading === true || (!showOAuth && !state.token)"
|
||||
:class="{ loading }"
|
||||
@click="onConnectClick">
|
||||
<template #icon>
|
||||
<OpenInNewIcon :size="20" />
|
||||
</template>
|
||||
|
@ -86,7 +87,7 @@ import GithubIcon from './icons/GithubIcon.vue'
|
|||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { delay } from '../utils.js'
|
||||
import { oauthConnect } from '../utils.js'
|
||||
import { showSuccess, showError } from '@nextcloud/dialogs'
|
||||
|
||||
import NcButton from '@nextcloud/vue/dist/Components/Button.js'
|
||||
|
@ -112,7 +113,7 @@ export default {
|
|||
return {
|
||||
state: loadState('integration_github', 'user-config'),
|
||||
readonly: true,
|
||||
redirect_uri: window.location.protocol + '//' + window.location.host + generateUrl('/apps/integration_github/oauth-redirect'),
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -149,63 +150,51 @@ export default {
|
|||
this.state[key] = newValue
|
||||
this.saveOptions({ [key]: this.state[key] ? '1' : '0' })
|
||||
},
|
||||
onInput() {
|
||||
delay(() => {
|
||||
this.saveOptions({ token: this.state.token })
|
||||
}, 2000)()
|
||||
},
|
||||
saveOptions(values) {
|
||||
const req = {
|
||||
values,
|
||||
}
|
||||
const url = generateUrl('/apps/integration_github/config')
|
||||
axios.put(url, req)
|
||||
.then((response) => {
|
||||
showSuccess(t('integration_github', 'GitHub options saved'))
|
||||
if (response.data.user_name !== undefined) {
|
||||
this.state.user_name = response.data.user_name
|
||||
if (this.state.token && response.data.user_name === '') {
|
||||
showError(t('integration_github', 'Incorrect access token'))
|
||||
}
|
||||
axios.put(url, req).then((response) => {
|
||||
showSuccess(t('integration_github', 'GitHub options saved'))
|
||||
if (response.data.user_name !== undefined) {
|
||||
this.state.user_name = response.data.user_name
|
||||
if (this.state.token && response.data.user_name === '') {
|
||||
showError(t('integration_github', 'Incorrect access token'))
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
showError(
|
||||
t('integration_github', 'Failed to save GitHub options')
|
||||
+ ': ' + error.response?.request?.responseText
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
})
|
||||
}
|
||||
}).catch((error) => {
|
||||
showError(
|
||||
t('integration_github', 'Failed to save GitHub options')
|
||||
+ ': ' + error.response?.request?.responseText
|
||||
)
|
||||
}).then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
onOAuthClick() {
|
||||
const oauthState = Math.random().toString(36).substring(3)
|
||||
const requestUrl = 'https://github.com/login/oauth/authorize'
|
||||
+ '?client_id=' + encodeURIComponent(this.state.client_id)
|
||||
+ '&redirect_uri=' + encodeURIComponent(this.redirect_uri)
|
||||
+ '&state=' + encodeURIComponent(oauthState)
|
||||
+ '&scope=' + encodeURIComponent('read:user user:email repo notifications')
|
||||
|
||||
const req = {
|
||||
values: {
|
||||
oauth_state: oauthState,
|
||||
redirect_uri: this.redirect_uri,
|
||||
oauth_origin: 'settings',
|
||||
},
|
||||
onConnectClick() {
|
||||
if (this.showOAuth) {
|
||||
this.connectWithOauth()
|
||||
} else {
|
||||
this.connectWithToken()
|
||||
}
|
||||
},
|
||||
connectWithToken() {
|
||||
this.loading = true
|
||||
this.saveOptions({
|
||||
token: this.state.token,
|
||||
})
|
||||
},
|
||||
connectWithOauth() {
|
||||
if (this.state.use_popup) {
|
||||
oauthConnect(this.state.client_id, null, true)
|
||||
.then((data) => {
|
||||
this.state.token = 'dummyToken'
|
||||
this.state.user_name = data.userName
|
||||
})
|
||||
} else {
|
||||
oauthConnect(this.state.client_id, 'settings')
|
||||
}
|
||||
const url = generateUrl('/apps/integration_github/config')
|
||||
axios.put(url, req)
|
||||
.then((response) => {
|
||||
window.location.replace(requestUrl)
|
||||
})
|
||||
.catch((error) => {
|
||||
showError(
|
||||
t('integration_github', 'Failed to save GitHub OAuth state')
|
||||
+ ': ' + error.response?.request?.responseText
|
||||
)
|
||||
})
|
||||
.then(() => {
|
||||
})
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
const state = loadState('integration_github', 'popup-data')
|
||||
const userName = state.user_name
|
||||
const userDisplayName = state.user_displayname
|
||||
|
||||
if (window.opener) {
|
||||
window.opener.postMessage({ userName, userDisplayName })
|
||||
window.close()
|
||||
}
|
47
src/utils.js
47
src/utils.js
|
@ -1,3 +1,7 @@
|
|||
import { generateUrl } from '@nextcloud/router'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
let mytimer = 0
|
||||
export function delay(callback, ms) {
|
||||
return function() {
|
||||
|
@ -9,3 +13,46 @@ export function delay(callback, ms) {
|
|||
}, ms || 0)
|
||||
}
|
||||
}
|
||||
|
||||
export function oauthConnect(clientId, oauthOrigin, usePopup = false) {
|
||||
const redirectUri = window.location.protocol + '//' + window.location.host + generateUrl('/apps/integration_github/oauth-redirect')
|
||||
|
||||
const oauthState = Math.random().toString(36).substring(3)
|
||||
const requestUrl = 'https://github.com/login/oauth/authorize'
|
||||
+ '?client_id=' + encodeURIComponent(clientId)
|
||||
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
|
||||
+ '&state=' + encodeURIComponent(oauthState)
|
||||
+ '&scope=' + encodeURIComponent('read:user user:email repo notifications')
|
||||
|
||||
const req = {
|
||||
values: {
|
||||
oauth_state: oauthState,
|
||||
redirect_uri: redirectUri,
|
||||
oauth_origin: usePopup ? undefined : oauthOrigin,
|
||||
},
|
||||
}
|
||||
const url = generateUrl('/apps/integration_github/config')
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.put(url, req).then((response) => {
|
||||
if (usePopup) {
|
||||
const ssoWindow = window.open(
|
||||
requestUrl,
|
||||
t('integration_github', 'Connect to GitHub'),
|
||||
'toolbar=no, menubar=no, width=600, height=700')
|
||||
ssoWindow.focus()
|
||||
window.addEventListener('message', (event) => {
|
||||
console.debug('Child window message received', event)
|
||||
resolve(event.data)
|
||||
})
|
||||
} else {
|
||||
window.location.replace(requestUrl)
|
||||
}
|
||||
}).catch((error) => {
|
||||
showError(
|
||||
t('integration_github', 'Failed to save GitHub OAuth state')
|
||||
+ ': ' + (error.response?.request?.responseText ?? '')
|
||||
)
|
||||
console.error(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ import CheckIcon from 'vue-material-design-icons/Check.vue'
|
|||
import CloseIcon from 'vue-material-design-icons/Close.vue'
|
||||
|
||||
import GithubIcon from '../components/icons/GithubIcon.vue'
|
||||
import { oauthConnect } from '../utils.js'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl, imagePath } from '@nextcloud/router'
|
||||
|
@ -160,30 +161,15 @@ export default {
|
|||
|
||||
methods: {
|
||||
onOAuthClick() {
|
||||
const redirectUri = window.location.protocol + '//' + window.location.host + generateUrl('/apps/integration_github/oauth-redirect')
|
||||
const oauthState = Math.random().toString(36).substring(3)
|
||||
const requestUrl = 'https://github.com/login/oauth/authorize'
|
||||
+ '?client_id=' + encodeURIComponent(this.initialState.client_id)
|
||||
+ '&redirect_uri=' + encodeURIComponent(redirectUri)
|
||||
+ '&state=' + encodeURIComponent(oauthState)
|
||||
+ '&scope=' + encodeURIComponent('read:user user:email repo notifications')
|
||||
|
||||
const req = {
|
||||
values: {
|
||||
oauth_state: oauthState,
|
||||
redirect_uri: redirectUri,
|
||||
oauth_origin: 'dashboard',
|
||||
},
|
||||
if (this.initialState.use_popup) {
|
||||
oauthConnect(this.initialState.client_id, null, true)
|
||||
.then((data) => {
|
||||
this.stopLoop()
|
||||
this.launchLoop()
|
||||
})
|
||||
} else {
|
||||
oauthConnect(this.initialState.client_id, 'dashboard')
|
||||
}
|
||||
const url = generateUrl('/apps/integration_github/config')
|
||||
axios.put(url, req).then((response) => {
|
||||
window.location.replace(requestUrl)
|
||||
}).catch((error) => {
|
||||
showError(
|
||||
t('integration_github', 'Failed to save GitHub OAuth state')
|
||||
+ ': ' + error.response?.request?.responseText
|
||||
)
|
||||
})
|
||||
},
|
||||
changeWindowVisibility() {
|
||||
this.windowVisibility = !document.hidden
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
$appId = OCA\Github\AppInfo\Application::APP_ID;
|
||||
\OCP\Util::addScript($appId, $appId . '-popupSuccess');
|
||||
?>
|
||||
|
||||
<div id="github_prefs"></div>
|
|
@ -18,6 +18,7 @@ webpackConfig.entry = {
|
|||
personalSettings: { import: path.join(__dirname, 'src', 'personalSettings.js'), filename: appId + '-personalSettings.js' },
|
||||
adminSettings: { import: path.join(__dirname, 'src', 'adminSettings.js'), filename: appId + '-adminSettings.js' },
|
||||
dashboard: { import: path.join(__dirname, 'src', 'dashboard.js'), filename: appId + '-dashboard.js' },
|
||||
popupSuccess: { import: path.join(__dirname, 'src', 'popupSuccess.js'), filename: appId + '-popupSuccess.js' },
|
||||
}
|
||||
|
||||
webpackConfig.plugins.push(
|
||||
|
|
Загрузка…
Ссылка в новой задаче