Signed-off-by: Julien Veyssier <eneiluj@posteo.net>
This commit is contained in:
Julien Veyssier 2022-08-26 13:08:39 +02:00
Родитель bae3ff88a8
Коммит 782804e23f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4141FEE162030638
11 изменённых файлов: 181 добавлений и 105 удалений

6
.github/workflows/release.yml поставляемый
Просмотреть файл

@ -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(() => {
})
},
},
}

10
src/popupSuccess.js Normal file
Просмотреть файл

@ -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()
}

Просмотреть файл

@ -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(