diff --git a/apps/settings/src/components/AuthToken.vue b/apps/settings/src/components/AuthToken.vue index 4e0a94543be..12d801d0db1 100644 --- a/apps/settings/src/components/AuthToken.vue +++ b/apps/settings/src/components/AuthToken.vue @@ -2,6 +2,7 @@ - @copyright 2019 Christoph Wurst - - @author 2019 Christoph Wurst + - @author Ferdinand Thiessen - - @license GNU AGPL version 3 or any later version - @@ -20,34 +21,43 @@ --> {{ t('settings', 'Revoking this token might prevent the wiping of your device if it has not started the wipe yet.') }} @@ -83,10 +93,21 @@ - diff --git a/apps/settings/src/components/AuthTokenList.vue b/apps/settings/src/components/AuthTokenList.vue index 18b4c87d565..e4759adba8d 100644 --- a/apps/settings/src/components/AuthTokenList.vue +++ b/apps/settings/src/components/AuthTokenList.vue @@ -2,6 +2,7 @@ - @copyright 2019 Christoph Wurst - - @author 2019 Christoph Wurst + - @author Ferdinand Thiessen - - @license GNU AGPL version 3 or any later version - @@ -20,115 +21,74 @@ --> - - - - diff --git a/apps/settings/src/components/AuthTokenSection.vue b/apps/settings/src/components/AuthTokenSection.vue index bb9bd3fb065..a1689846130 100644 --- a/apps/settings/src/components/AuthTokenSection.vue +++ b/apps/settings/src/components/AuthTokenSection.vue @@ -2,6 +2,7 @@ - @copyright 2019 Christoph Wurst - - @author 2019 Christoph Wurst + - @author Ferdinand Thiessen - - @license GNU AGPL version 3 or any later version - @@ -25,164 +26,32 @@

{{ t('settings', 'Web, desktop and mobile clients currently logged in to your account.') }}

- - + + - - - diff --git a/apps/settings/src/components/AuthTokenSetup.vue b/apps/settings/src/components/AuthTokenSetup.vue new file mode 100644 index 00000000000..9e709397362 --- /dev/null +++ b/apps/settings/src/components/AuthTokenSetup.vue @@ -0,0 +1,114 @@ + + + + + + + diff --git a/apps/settings/src/components/AuthTokenSetupDialog.vue b/apps/settings/src/components/AuthTokenSetupDialog.vue new file mode 100644 index 00000000000..f40fe722cef --- /dev/null +++ b/apps/settings/src/components/AuthTokenSetupDialog.vue @@ -0,0 +1,220 @@ + + + + + + diff --git a/apps/settings/src/components/AuthTokenSetupDialogue.vue b/apps/settings/src/components/AuthTokenSetupDialogue.vue deleted file mode 100644 index 18fa0f3ab2f..00000000000 --- a/apps/settings/src/components/AuthTokenSetupDialogue.vue +++ /dev/null @@ -1,239 +0,0 @@ - - - - - - - diff --git a/apps/settings/src/main-personal-security.js b/apps/settings/src/main-personal-security.js index 634deca61b5..d2aef1039ea 100644 --- a/apps/settings/src/main-personal-security.js +++ b/apps/settings/src/main-personal-security.js @@ -3,6 +3,7 @@ * * @author Christoph Wurst * @author John Molakvoæ + * @author Ferdinand Thiessen * * @license AGPL-3.0-or-later * @@ -21,22 +22,23 @@ * */ -import { loadState } from '@nextcloud/initial-state' import Vue from 'vue' import VTooltip from 'v-tooltip' import AuthTokenSection from './components/AuthTokenSection.vue' +import { getRequestToken } from '@nextcloud/auth' +import { PiniaVuePlugin, createPinia } from 'pinia' + +import '@nextcloud/password-confirmation/dist/style.css' // eslint-disable-next-line camelcase -__webpack_nonce__ = btoa(OC.requestToken) +__webpack_nonce__ = btoa(getRequestToken()) +const pinia = createPinia() + +Vue.use(PiniaVuePlugin) Vue.use(VTooltip, { defaultHtml: false }) Vue.prototype.t = t const View = Vue.extend(AuthTokenSection) -new View({ - propsData: { - tokens: loadState('settings', 'app_tokens'), - canCreateToken: loadState('settings', 'can_create_app_token'), - }, -}).$mount('#security-authtokens') +new View({ pinia }).$mount('#security-authtokens') diff --git a/apps/settings/src/store/authtoken.ts b/apps/settings/src/store/authtoken.ts new file mode 100644 index 00000000000..399c39faae7 --- /dev/null +++ b/apps/settings/src/store/authtoken.ts @@ -0,0 +1,214 @@ +/** + * @copyright 2023 Ferdinand Thiessen + * + * @author Ferdinand Thiessen + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +import { showError } from '@nextcloud/dialogs' +import { loadState } from '@nextcloud/initial-state' +import { translate as t } from '@nextcloud/l10n' +import { confirmPassword } from '@nextcloud/password-confirmation' +import { generateUrl } from '@nextcloud/router' +import { defineStore } from 'pinia' + +import axios from '@nextcloud/axios' +import logger from '../logger' + +const BASE_URL = generateUrl('/settings/personal/authtokens') + +const confirm = () => { + return new Promise(resolve => { + window.OC.dialogs.confirm( + t('settings', 'Do you really want to wipe your data from this device?'), + t('settings', 'Confirm wipe'), + resolve, + true, + ) + }) +} + +export enum TokenType { + TEMPORARY_TOKEN = 0, + PERMANENT_TOKEN = 1, + WIPING_TOKEN = 2, +} + +export interface IToken { + id: number + canDelete: boolean + canRename: boolean + current?: true + /** + * Last activity as UNIX timestamp (in seconds) + */ + lastActivity: number + name: string + type: TokenType + scope: Record +} + +export interface ITokenResponse { + /** + * The device token created + */ + deviceToken: IToken + /** + * User who is assigned with this token + */ + loginName: string + /** + * The token for authentication + */ + token: string +} + +export const useAuthTokenStore = defineStore('auth-token', { + state() { + return { + tokens: loadState('settings', 'app_tokens', []), + } + }, + actions: { + /** + * Update a token on server + * @param token Token to update + */ + async updateToken(token: IToken) { + const { data } = await axios.put(`${BASE_URL}/${token.id}`, token) + return data + }, + + /** + * Add a new token + * @param name The token name + */ + async addToken(name: string) { + logger.debug('Creating a new app token') + + try { + await confirmPassword() + + const { data } = await axios.post(BASE_URL, { name }) + this.tokens.push(data.deviceToken) + logger.debug('App token created') + return data + } catch (error) { + return null + } + }, + + /** + * Delete a given app token + * @param token Token to delete + */ + async deleteToken(token: IToken) { + logger.debug('Deleting app token', { token }) + + this.tokens = this.tokens.filter(({ id }) => id !== token.id) + + try { + await axios.delete(`${BASE_URL}/${token.id}`) + logger.debug('App token deleted') + return true + } catch (error) { + logger.error('Could not delete app token', { error }) + showError(t('settings', 'Could not delete the app token')) + // Restore + this.tokens.push(token) + } + return false + }, + + /** + * Wipe a token and the connected device + * @param token Token to wipe + */ + async wipeToken(token: IToken) { + logger.debug('Wiping app token', { token }) + + try { + await confirmPassword() + + if (!(await confirm())) { + logger.debug('Wipe aborted by user') + return + } + + await axios.post(`${BASE_URL}/wipe/${token.id}`) + logger.debug('App token marked for wipe', { token }) + + token.type = TokenType.WIPING_TOKEN + return true + } catch (error) { + logger.error('Could not wipe app token', { error }) + showError(t('settings', 'Error while wiping the device with the token')) + } + return false + }, + + /** + * Rename an existing token + * @param token The token to rename + * @param newName The new name to set + */ + async renameToken(token: IToken, newName: string) { + logger.debug(`renaming app token ${token.id} from ${token.name} to '${newName}'`) + + const oldName = token.name + token.name = newName + + try { + await this.updateToken(token) + logger.debug('App token name updated') + return true + } catch (error) { + logger.error('Could not update app token name', { error }) + showError(t('settings', 'Error while updating device token name')) + // Restore + token.name = oldName + } + return false + }, + + /** + * Set scope of the token + * @param token Token to set scope + * @param scope scope to set + * @param value value to set + */ + async setTokenScope(token: IToken, scope: string, value: boolean) { + logger.debug('Updating app token scope', { token, scope, value }) + + const oldVal = token.scope[scope] + token.scope[scope] = value + + try { + await this.updateToken(token) + logger.debug('app token scope updated') + return true + } catch (error) { + logger.error('could not update app token scope', { error }) + showError(t('settings', 'Error while updating device token scope')) + // Restore + token.scope[scope] = oldVal + } + return false + }, + }, + +}) diff --git a/package.json b/package.json index 3bf34b37383..6d593085d4e 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "license": "AGPL-3.0-or-later", "dependencies": { "@chenfengyuan/vue-qrcode": "^1.0.2", + "@mdi/js": "^7.3.67", "@mdi/svg": "^7.3.67", "@nextcloud/auth": "^2.1.0", "@nextcloud/axios": "^2.3.0",