Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>tags/v28.0.2rc1
@@ -2,6 +2,7 @@ | |||
- @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- | |||
- @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- @author Ferdinand Thiessen <opensource@fthiessen.de> | |||
- | |||
- @license GNU AGPL version 3 or any later version | |||
- | |||
@@ -20,34 +21,43 @@ | |||
--> | |||
<template> | |||
<tr :data-id="token.id" | |||
:class="wiping"> | |||
<td class="client"> | |||
<div :class="iconName.icon" /> | |||
</td> | |||
<td class="token-name"> | |||
<NcTextField v-if="token.canRename && renaming" | |||
ref="input" | |||
v-model="newName" | |||
type="text" | |||
:label="t('settings', 'Device name')" | |||
@keyup.enter="rename" | |||
@change="rename" | |||
@keyup.esc="cancelRename" /> | |||
<span v-else>{{ iconName.name }}</span> | |||
<span v-if="wiping" class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span> | |||
<tr :class="['auth-token', { 'auth-token--wiping': wiping }]" :data-id="token.id"> | |||
<td class="auth-token__name"> | |||
<NcIconSvgWrapper :path="tokenIcon" /> | |||
<div class="auth-token__name-wrapper"> | |||
<form v-if="token.canRename && renaming" | |||
class="auth-token__name-form" | |||
@submit.prevent.stop="rename"> | |||
<NcTextField ref="input" | |||
:value.sync="newName" | |||
:label="t('settings', 'Device name')" | |||
:show-trailing-button="true" | |||
:trailing-button-label="t('settings', 'Cancel renaming')" | |||
@trailing-button-click="cancelRename" | |||
@keyup.esc="cancelRename" /> | |||
<NcButton :aria-label="t('settings', 'Save new name')" type="tertiary" native-type="submit"> | |||
<template #icon> | |||
<NcIconSvgWrapper :path="mdiCheck" /> | |||
</template> | |||
</NcButton> | |||
</form> | |||
<span v-else>{{ tokenLabel }}</span> | |||
<span v-if="wiping" class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span> | |||
</div> | |||
</td> | |||
<td> | |||
<span :title="lastActivity" class="last-activity">{{ lastActivityRelative }}</span> | |||
<NcDateTime class="auth-token__last-activity" | |||
:ignore-seconds="true" | |||
:timestamp="tokenLastActivity" /> | |||
</td> | |||
<td class="more"> | |||
<td class="auth-token__actions"> | |||
<NcActions v-if="!token.current" | |||
:title="t('settings', 'Device settings')" | |||
:aria-label="t('settings', 'Device settings')" | |||
:open.sync="actionOpen"> | |||
<NcActionCheckbox v-if="token.type === 1" | |||
<NcActionCheckbox v-if="canChangeScope" | |||
:checked="token.scope.filesystem" | |||
@change.stop.prevent="$emit('toggle-scope', token, 'filesystem', !token.scope.filesystem)"> | |||
@update:checked="updateFileSystemScope"> | |||
<!-- TODO: add text/longtext with some description --> | |||
{{ t('settings', 'Allow filesystem access') }} | |||
</NcActionCheckbox> | |||
@@ -73,7 +83,7 @@ | |||
</template> | |||
<NcActionButton v-else-if="token.type === 2" | |||
icon="icon-delete" | |||
:title="t('settings', 'Revoke')" | |||
:name="t('settings', 'Revoke')" | |||
@click.stop.prevent="revoke"> | |||
{{ t('settings', 'Revoking this token might prevent the wiping of your device if it has not started the wipe yet.') }} | |||
</NcActionButton> | |||
@@ -83,10 +93,21 @@ | |||
</tr> | |||
</template> | |||
<script> | |||
<script lang="ts"> | |||
import type { PropType } from 'vue' | |||
import type { IToken } from '../store/authtoken' | |||
import { mdiCheck, mdiCellphone, mdiTablet, mdiMonitor, mdiWeb, mdiKey, mdiMicrosoftEdge, mdiFirefox, mdiGoogleChrome, mdiAppleSafari, mdiAndroid, mdiAppleIos } from '@mdi/js' | |||
import { translate as t } from '@nextcloud/l10n' | |||
import { defineComponent } from 'vue' | |||
import { TokenType, useAuthTokenStore } from '../store/authtoken.ts' | |||
import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' | |||
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' | |||
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox.js' | |||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' | |||
import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js' | |||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' | |||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' | |||
// When using capture groups the following parts are extracted the first is used as the version number, the second as the OS | |||
@@ -118,116 +139,162 @@ const userAgentMap = { | |||
neon: /Neon \d+\.\d+\.\d+\+\d+/, | |||
} | |||
const nameMap = { | |||
ie: t('setting', 'Internet Explorer'), | |||
edge: t('setting', 'Edge'), | |||
firefox: t('setting', 'Firefox'), | |||
chrome: t('setting', 'Google Chrome'), | |||
safari: t('setting', 'Safari'), | |||
androidChrome: t('setting', 'Google Chrome for Android'), | |||
iphone: t('setting', 'iPhone'), | |||
ipad: t('setting', 'iPad'), | |||
iosClient: t('setting', '{productName} iOS app', { productName: window.oc_defaults.productName }), | |||
androidClient: t('setting', '{productName} Android app', { productName: window.oc_defaults.productName }), | |||
iosTalkClient: t('setting', '{productName} Talk for iOS', { productName: window.oc_defaults.productName }), | |||
androidTalkClient: t('setting', '{productName} Talk for Android', { productName: window.oc_defaults.productName }), | |||
edge: 'Microsoft Edge', | |||
firefox: 'Firefox', | |||
chrome: 'Google Chrome', | |||
safari: 'Safari', | |||
androidChrome: t('settings', 'Google Chrome for Android'), | |||
iphone: 'iPhone', | |||
ipad: 'iPad', | |||
iosClient: t('settings', '{productName} iOS app', { productName: window.oc_defaults.productName }), | |||
androidClient: t('settings', '{productName} Android app', { productName: window.oc_defaults.productName }), | |||
iosTalkClient: t('settings', '{productName} Talk for iOS', { productName: window.oc_defaults.productName }), | |||
androidTalkClient: t('settings', '{productName} Talk for Android', { productName: window.oc_defaults.productName }), | |||
syncClient: t('settings', 'Sync client'), | |||
davx5: 'DAVx5', | |||
webPirate: 'WebPirate', | |||
sailfishBrowser: 'SailfishBrowser', | |||
neon: 'Neon', | |||
} | |||
const iconMap = { | |||
ie: 'icon-desktop', | |||
edge: 'icon-desktop', | |||
firefox: 'icon-desktop', | |||
chrome: 'icon-desktop', | |||
safari: 'icon-desktop', | |||
androidChrome: 'icon-phone', | |||
iphone: 'icon-phone', | |||
ipad: 'icon-tablet', | |||
iosClient: 'icon-phone', | |||
androidClient: 'icon-phone', | |||
iosTalkClient: 'icon-phone', | |||
androidTalkClient: 'icon-phone', | |||
davx5: 'icon-phone', | |||
webPirate: 'icon-link', | |||
sailfishBrowser: 'icon-link', | |||
} | |||
export default { | |||
export default defineComponent({ | |||
name: 'AuthToken', | |||
components: { | |||
NcActions, | |||
NcActionButton, | |||
NcActionCheckbox, | |||
NcButton, | |||
NcDateTime, | |||
NcIconSvgWrapper, | |||
NcTextField, | |||
}, | |||
props: { | |||
token: { | |||
type: Object, | |||
type: Object as PropType<IToken>, | |||
required: true, | |||
}, | |||
}, | |||
setup() { | |||
const authTokenStore = useAuthTokenStore() | |||
return { authTokenStore } | |||
}, | |||
data() { | |||
return { | |||
showMore: this.token.canScope || this.token.canDelete, | |||
actionOpen: false, | |||
renaming: false, | |||
newName: '', | |||
oldName: '', | |||
actionOpen: false, | |||
mdiCheck, | |||
} | |||
}, | |||
computed: { | |||
lastActivityRelative() { | |||
return OC.Util.relativeModifiedDate(this.token.lastActivity * 1000) | |||
}, | |||
lastActivity() { | |||
return OC.Util.formatDate(this.token.lastActivity * 1000, 'LLL') | |||
canChangeScope() { | |||
return this.token.type === TokenType.PERMANENT_TOKEN | |||
}, | |||
iconName() { | |||
/** | |||
* Object ob the current user agend used by the token | |||
* @return Either an object containing user agent information or null if unknown | |||
*/ | |||
client() { | |||
// pretty format sync client user agent | |||
const matches = this.token.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/) | |||
let icon = '' | |||
if (matches) { | |||
/* eslint-disable-next-line */ | |||
this.token.name = t('settings', 'Sync client - {os}', { | |||
return { | |||
id: 'syncClient', | |||
os: matches[1], | |||
version: matches[2], | |||
}) | |||
icon = 'icon-desktop' | |||
} | |||
} | |||
// preserve title for cases where we format it further | |||
const title = this.token.name | |||
let name = this.token.name | |||
for (const client in userAgentMap) { | |||
const matches = title.match(userAgentMap[client]) | |||
const matches = this.token.name.match(userAgentMap[client]) | |||
if (matches) { | |||
if (matches[2] && matches[1]) { // version number and os | |||
name = nameMap[client] + ' ' + matches[2] + ' - ' + matches[1] | |||
} else if (matches[1]) { // only version number | |||
name = nameMap[client] + ' ' + matches[1] | |||
} else { | |||
name = nameMap[client] | |||
return { | |||
id: client, | |||
os: matches[2] && matches[1], | |||
version: matches[2] ?? matches[1], | |||
} | |||
icon = iconMap[client] | |||
} | |||
} | |||
return null | |||
}, | |||
/** | |||
* Last activity of the token as ECMA timestamp (in ms) | |||
*/ | |||
tokenLastActivity() { | |||
return this.token.lastActivity * 1000 | |||
}, | |||
/** | |||
* Icon to use for the current token | |||
*/ | |||
tokenIcon() { | |||
// For custom created app tokens / app passwords | |||
if (this.token.type === TokenType.PERMANENT_TOKEN) { | |||
return mdiKey | |||
} | |||
switch (this.client?.id) { | |||
case 'edge': | |||
return mdiMicrosoftEdge | |||
case 'firefox': | |||
return mdiFirefox | |||
case 'chrome': | |||
return mdiGoogleChrome | |||
case 'safari': | |||
return mdiAppleSafari | |||
case 'androidChrome': | |||
case 'androidClient': | |||
case 'androidTalkClient': | |||
return mdiAndroid | |||
case 'iphone': | |||
case 'iosClient': | |||
case 'iosTalkClient': | |||
return mdiAppleIos | |||
case 'ipad': | |||
return mdiTablet | |||
case 'davx5': | |||
return mdiCellphone | |||
case 'syncClient': | |||
return mdiMonitor | |||
case 'webPirate': | |||
case 'sailfishBrowser': | |||
default: | |||
return mdiWeb | |||
} | |||
}, | |||
/** | |||
* Label to be shown for current token | |||
*/ | |||
tokenLabel() { | |||
if (this.token.current) { | |||
name = t('settings', 'This session') | |||
return t('settings', 'This session') | |||
} | |||
if (this.client === null) { | |||
return this.token.name | |||
} | |||
return { | |||
icon, | |||
name, | |||
const name = nameMap[this.client.id] | |||
if (this.client.os) { | |||
return t('settings', '{client} - {version} ({system})', { client: name, system: this.client.os, version: this.client.version }) | |||
} else if (this.client.version) { | |||
return t('settings', '{client} - {version}', { client: name, version: this.client.version }) | |||
} | |||
return name | |||
}, | |||
/** | |||
* If the current token is considered for remote wiping | |||
*/ | |||
wiping() { | |||
return this.token.type === 2 | |||
return this.token.type === TokenType.WIPING_TOKEN | |||
}, | |||
}, | |||
methods: { | |||
t, | |||
updateFileSystemScope(state: boolean) { | |||
this.authTokenStore.setTokenScope(this.token, 'filesystem', state) | |||
}, | |||
startRename() { | |||
// Close action (popover menu) | |||
this.actionOpen = false | |||
@@ -236,77 +303,69 @@ export default { | |||
this.newName = this.token.name | |||
this.renaming = true | |||
this.$nextTick(() => { | |||
this.$refs.input.select() | |||
this.$refs.input!.select() | |||
}) | |||
}, | |||
cancelRename() { | |||
this.renaming = false | |||
this.$emit('rename', this.token, this.oldName) | |||
}, | |||
revoke() { | |||
this.actionOpen = false | |||
this.$emit('delete', this.token) | |||
this.authTokenStore.deleteToken(this.token) | |||
}, | |||
rename() { | |||
this.renaming = false | |||
this.$emit('rename', this.token, this.newName) | |||
this.authTokenStore.renameToken(this.token, this.newName) | |||
}, | |||
wipe() { | |||
this.actionOpen = false | |||
this.$emit('wipe', this.token) | |||
this.authTokenStore.wipeToken(this.token) | |||
}, | |||
}, | |||
} | |||
}) | |||
</script> | |||
<style lang="scss" scoped> | |||
.wiping { | |||
background-color: var(--color-background-darker); | |||
} | |||
td { | |||
border-top: 1px solid var(--color-border); | |||
max-width: 200px; | |||
white-space: normal; | |||
vertical-align: middle; | |||
position: relative; | |||
.auth-token { | |||
border-top: 2px solid var(--color-border); | |||
max-width: 200px; | |||
white-space: normal; | |||
vertical-align: middle; | |||
position: relative; | |||
&%icon { | |||
overflow: visible; | |||
position: relative; | |||
width: 44px; | |||
height: 44px; | |||
} | |||
&--wiping { | |||
background-color: var(--color-background-dark); | |||
} | |||
&.token-name { | |||
padding: 10px 6px; | |||
&__name { | |||
padding-block: 10px; | |||
display: flex; | |||
align-items: center; | |||
gap: 6px; | |||
min-width: 355px; // ensure no jumping when renaming | |||
} | |||
&.token-rename { | |||
padding: 0; | |||
} | |||
&__name-wrapper { | |||
display: flex; | |||
flex-direction: column; | |||
} | |||
input { | |||
width: 100%; | |||
margin: 0; | |||
} | |||
} | |||
&.token-name .wiping-warning { | |||
color: var(--color-text-lighter); | |||
} | |||
&__name-form { | |||
align-items: end; | |||
display: flex; | |||
gap: 4px; | |||
} | |||
&.more { | |||
@extend %icon; | |||
padding: 0 10px; | |||
} | |||
&__actions { | |||
padding: 0 10px; | |||
} | |||
&.client { | |||
@extend %icon; | |||
&__last-activity { | |||
padding-inline-start: 10px; | |||
} | |||
div { | |||
opacity: 0.57; | |||
width: 44px; | |||
height: 44px; | |||
} | |||
} | |||
.wiping-warning { | |||
color: var(--color-text-maxcontrast); | |||
} | |||
} | |||
</style> |
@@ -2,6 +2,7 @@ | |||
- @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- | |||
- @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- @author Ferdinand Thiessen <opensource@fthiessen.de> | |||
- | |||
- @license GNU AGPL version 3 or any later version | |||
- | |||
@@ -20,115 +21,74 @@ | |||
--> | |||
<template> | |||
<table id="app-tokens-table"> | |||
<thead v-if="tokens.length"> | |||
<table id="app-tokens-table" class="token-list"> | |||
<thead> | |||
<tr> | |||
<th /> | |||
<th>{{ t('settings', 'Device') }}</th> | |||
<th>{{ t('settings', 'Last activity') }}</th> | |||
<th /> | |||
<th class="token-list__header-device"> | |||
{{ t('settings', 'Device') }} | |||
</th> | |||
<th class="toke-list__header-activity"> | |||
{{ t('settings', 'Last activity') }} | |||
</th> | |||
<th> | |||
<span class="hidden-visually"> | |||
{{ t('settings', 'Actions') }} | |||
</span> | |||
</th> | |||
</tr> | |||
</thead> | |||
<tbody class="token-list"> | |||
<tbody class="token-list__body"> | |||
<AuthToken v-for="token in sortedTokens" | |||
:key="token.id" | |||
:token="token" | |||
@toggle-scope="toggleScope" | |||
@rename="rename" | |||
@delete="onDelete" | |||
@wipe="onWipe" /> | |||
:token="token" /> | |||
</tbody> | |||
</table> | |||
</template> | |||
<script> | |||
<script lang="ts"> | |||
import { translate as t } from '@nextcloud/l10n' | |||
import { defineComponent } from 'vue' | |||
import { useAuthTokenStore } from '../store/authtoken' | |||
import AuthToken from './AuthToken.vue' | |||
export default { | |||
export default defineComponent({ | |||
name: 'AuthTokenList', | |||
components: { | |||
AuthToken, | |||
}, | |||
props: { | |||
tokens: { | |||
type: Array, | |||
required: true, | |||
}, | |||
setup() { | |||
const authTokenStore = useAuthTokenStore() | |||
return { authTokenStore } | |||
}, | |||
computed: { | |||
sortedTokens() { | |||
return this.tokens.slice().sort((t1, t2) => { | |||
const ts1 = parseInt(t1.lastActivity, 10) | |||
const ts2 = parseInt(t2.lastActivity, 10) | |||
return ts2 - ts1 | |||
}) | |||
return [...this.authTokenStore.tokens].sort((t1, t2) => t2.lastActivity - t1.lastActivity) | |||
}, | |||
}, | |||
methods: { | |||
toggleScope(token, scope, value) { | |||
// Just pass it on | |||
this.$emit('toggle-scope', token, scope, value) | |||
}, | |||
rename(token, newName) { | |||
// Just pass it on | |||
this.$emit('rename', token, newName) | |||
}, | |||
onDelete(token) { | |||
// Just pass it on | |||
this.$emit('delete', token) | |||
}, | |||
onWipe(token) { | |||
// Just pass it on | |||
this.$emit('wipe', token) | |||
}, | |||
t, | |||
}, | |||
} | |||
}) | |||
</script> | |||
<style lang="scss" scoped> | |||
table { | |||
width: 100%; | |||
min-height: 50px; | |||
padding-top: 5px; | |||
max-width: 580px; | |||
.token-list { | |||
width: 100%; | |||
min-height: 50px; | |||
padding-top: 5px; | |||
max-width: fit-content; | |||
th { | |||
padding: 10px 0; | |||
} | |||
th { | |||
padding-block: 10px; | |||
padding-inline-start: 10px; | |||
} | |||
.token-list { | |||
td > a.icon-more { | |||
transition: opacity var(--animation-quick); | |||
} | |||
a.icon-more { | |||
padding: 14px; | |||
display: block; | |||
width: 44px; | |||
height: 44px; | |||
opacity: .5; | |||
} | |||
tr { | |||
&:hover td > a.icon, | |||
td > a.icon:focus, | |||
&.active td > a.icon { | |||
opacity: 1; | |||
} | |||
} | |||
#{&}__header-device { | |||
padding-inline-start: 50px; // 44px icon + 6px padding | |||
} | |||
</style> | |||
<!-- some styles are not scoped to make them work on subcomponents --> | |||
<style lang="scss"> | |||
#app-tokens-table { | |||
tr > *:nth-child(2) { | |||
padding-left: 6px; | |||
} | |||
tr > *:nth-child(3) { | |||
text-align: right; | |||
} | |||
&__header-activity { | |||
text-align: end; | |||
} | |||
} | |||
</style> |
@@ -2,6 +2,7 @@ | |||
- @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- | |||
- @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- @author Ferdinand Thiessen <opensource@fthiessen.de> | |||
- | |||
- @license GNU AGPL version 3 or any later version | |||
- | |||
@@ -25,164 +26,32 @@ | |||
<p class="settings-hint hidden-when-empty"> | |||
{{ t('settings', 'Web, desktop and mobile clients currently logged in to your account.') }} | |||
</p> | |||
<AuthTokenList :tokens="tokens" | |||
@toggle-scope="toggleTokenScope" | |||
@rename="rename" | |||
@delete="deleteToken" | |||
@wipe="wipeToken" /> | |||
<AuthTokenSetupDialogue v-if="canCreateToken" :add="addNewToken" /> | |||
<AuthTokenList /> | |||
<AuthTokenSetup v-if="canCreateToken" /> | |||
</div> | |||
</template> | |||
<script> | |||
import axios from '@nextcloud/axios' | |||
import { confirmPassword } from '@nextcloud/password-confirmation' | |||
import '@nextcloud/password-confirmation/dist/style.css' | |||
import { generateUrl } from '@nextcloud/router' | |||
<script lang="ts"> | |||
import { loadState } from '@nextcloud/initial-state' | |||
import { translate as t } from '@nextcloud/l10n' | |||
import { defineComponent } from 'vue' | |||
import AuthTokenList from './AuthTokenList.vue' | |||
import AuthTokenSetupDialogue from './AuthTokenSetupDialogue.vue' | |||
import AuthTokenSetup from './AuthTokenSetup.vue' | |||
const confirm = () => { | |||
return new Promise(resolve => { | |||
OC.dialogs.confirm( | |||
t('settings', 'Do you really want to wipe your data from this device?'), | |||
t('settings', 'Confirm wipe'), | |||
resolve, | |||
true, | |||
) | |||
}) | |||
} | |||
/** | |||
* Tap into a promise without losing the value | |||
* | |||
* @param {Function} cb the callback | |||
* @return {any} val the value | |||
*/ | |||
const tap = cb => val => { | |||
cb(val) | |||
return val | |||
} | |||
export default { | |||
export default defineComponent({ | |||
name: 'AuthTokenSection', | |||
components: { | |||
AuthTokenSetupDialogue, | |||
AuthTokenList, | |||
}, | |||
props: { | |||
tokens: { | |||
type: Array, | |||
required: true, | |||
}, | |||
canCreateToken: { | |||
type: Boolean, | |||
required: true, | |||
}, | |||
AuthTokenSetup, | |||
}, | |||
data() { | |||
return { | |||
baseUrl: generateUrl('/settings/personal/authtokens'), | |||
canCreateToken: loadState('settings', 'can_create_app_token'), | |||
} | |||
}, | |||
methods: { | |||
addNewToken(name) { | |||
console.debug('creating a new app token', name) | |||
const data = { | |||
name, | |||
} | |||
return axios.post(this.baseUrl, data) | |||
.then(resp => resp.data) | |||
.then(tap(() => console.debug('app token created'))) | |||
// eslint-disable-next-line vue/no-mutating-props | |||
.then(tap(data => this.tokens.push(data.deviceToken))) | |||
.catch(err => { | |||
console.error.bind('could not create app password', err) | |||
OC.Notification.showTemporary(t('settings', 'Error while creating device token')) | |||
throw err | |||
}) | |||
}, | |||
toggleTokenScope(token, scope, value) { | |||
console.debug('updating app token scope', token.id, scope, value) | |||
const oldVal = token.scope[scope] | |||
token.scope[scope] = value | |||
return this.updateToken(token) | |||
.then(tap(() => console.debug('app token scope updated'))) | |||
.catch(err => { | |||
console.error.bind('could not update app token scope', err) | |||
OC.Notification.showTemporary(t('settings', 'Error while updating device token scope')) | |||
// Restore | |||
token.scope[scope] = oldVal | |||
throw err | |||
}) | |||
}, | |||
rename(token, newName) { | |||
console.debug('renaming app token', token.id, token.name, newName) | |||
const oldName = token.name | |||
token.name = newName | |||
return this.updateToken(token) | |||
.then(tap(() => console.debug('app token name updated'))) | |||
.catch(err => { | |||
console.error.bind('could not update app token name', err) | |||
OC.Notification.showTemporary(t('settings', 'Error while updating device token name')) | |||
// Restore | |||
token.name = oldName | |||
}) | |||
}, | |||
updateToken(token) { | |||
return axios.put(this.baseUrl + '/' + token.id, token) | |||
.then(resp => resp.data) | |||
}, | |||
deleteToken(token) { | |||
console.debug('deleting app token', token) | |||
// eslint-disable-next-line vue/no-mutating-props | |||
this.tokens = this.tokens.filter(t => t !== token) | |||
return axios.delete(this.baseUrl + '/' + token.id) | |||
.then(resp => resp.data) | |||
.then(tap(() => console.debug('app token deleted'))) | |||
.catch(err => { | |||
console.error.bind('could not delete app token', err) | |||
OC.Notification.showTemporary(t('settings', 'Error while deleting the token')) | |||
// Restore | |||
// eslint-disable-next-line vue/no-mutating-props | |||
this.tokens.push(token) | |||
}) | |||
}, | |||
async wipeToken(token) { | |||
console.debug('wiping app token', token) | |||
try { | |||
await confirmPassword() | |||
if (!(await confirm())) { | |||
console.debug('wipe aborted by user') | |||
return | |||
} | |||
await axios.post(this.baseUrl + '/wipe/' + token.id) | |||
console.debug('app token marked for wipe') | |||
token.type = 2 | |||
} catch (err) { | |||
console.error('could not wipe app token', err) | |||
OC.Notification.showTemporary(t('settings', 'Error while wiping the device with the token')) | |||
} | |||
}, | |||
t, | |||
}, | |||
} | |||
}) | |||
</script> | |||
<style scoped> | |||
</style> |
@@ -0,0 +1,114 @@ | |||
<!-- | |||
- @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- | |||
- @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- @author Ferdinand Thiessen <opensource@fthiessen.de> | |||
- | |||
- @license GNU AGPL version 3 or any later version | |||
- | |||
- 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 <http://www.gnu.org/licenses/>. | |||
--> | |||
<template> | |||
<form id="generate-app-token-section" | |||
class="row spacing" | |||
@submit.prevent="submit"> | |||
<!-- Port to TextField component when available --> | |||
<NcTextField :value.sync="deviceName" | |||
type="text" | |||
:maxlength="120" | |||
:disabled="loading" | |||
class="app-name-text-field" | |||
:label="t('settings', 'App name')" | |||
:placeholder="t('settings', 'App name')" /> | |||
<NcButton type="primary" | |||
:disabled="loading || deviceName.length === 0" | |||
native-type="submit"> | |||
{{ t('settings', 'Create new app password') }} | |||
</NcButton> | |||
<AuthTokenSetupDialog :token="newToken" @close="newToken = null" /> | |||
</form> | |||
</template> | |||
<script lang="ts"> | |||
import { showError } from '@nextcloud/dialogs' | |||
import { translate as t } from '@nextcloud/l10n' | |||
import { defineComponent } from 'vue' | |||
import { useAuthTokenStore, type ITokenResponse } from '../store/authtoken' | |||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' | |||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' | |||
import AuthTokenSetupDialog from './AuthTokenSetupDialog.vue' | |||
import logger from '../logger' | |||
export default defineComponent({ | |||
name: 'AuthTokenSetup', | |||
components: { | |||
NcButton, | |||
NcTextField, | |||
AuthTokenSetupDialog, | |||
}, | |||
setup() { | |||
const authTokenStore = useAuthTokenStore() | |||
return { authTokenStore } | |||
}, | |||
data() { | |||
return { | |||
deviceName: '', | |||
loading: false, | |||
newToken: null as ITokenResponse|null, | |||
} | |||
}, | |||
methods: { | |||
t, | |||
reset() { | |||
this.loading = false | |||
this.deviceName = '' | |||
this.newToken = null | |||
}, | |||
async submit() { | |||
try { | |||
this.loading = true | |||
this.newToken = await this.authTokenStore.addToken(this.deviceName) | |||
} catch (error) { | |||
logger.error(error as Error) | |||
showError(t('settings', 'Error while creating device token')) | |||
this.reset() | |||
} finally { | |||
this.loading = false | |||
} | |||
}, | |||
}, | |||
}) | |||
</script> | |||
<style lang="scss" scoped> | |||
.app-name-text-field { | |||
height: 44px !important; | |||
padding-left: 12px; | |||
margin-right: 12px; | |||
width: 200px; | |||
} | |||
.row { | |||
display: flex; | |||
align-items: center; | |||
} | |||
.spacing { | |||
padding-top: 16px; | |||
} | |||
</style> |
@@ -0,0 +1,220 @@ | |||
<!-- | |||
- @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de> | |||
- | |||
- @author Ferdinand Thiessen <opensource@fthiessen.de> | |||
- | |||
- @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 <http://www.gnu.org/licenses/>. | |||
- | |||
--> | |||
<template> | |||
<NcDialog :open.sync="open" | |||
:name="t('settings', 'New app password')" | |||
content-classes="token-dialog"> | |||
<p> | |||
{{ t('settings', 'Use the credentials below to configure your app or device. For security reasons this password will only be shown once.') }} | |||
</p> | |||
<div class="token-dialog__name"> | |||
<NcTextField :label="t('settings', 'Username')" :value="loginName" readonly /> | |||
<NcButton type="tertiary" | |||
:title="copyLoginNameLabel" | |||
:aria-label="copyLoginNameLabel" | |||
@click="copyLoginName"> | |||
<template #icon> | |||
<NcIconSvgWrapper :path="copyNameIcon" /> | |||
</template> | |||
</NcButton> | |||
</div> | |||
<div class="token-dialog__password"> | |||
<NcTextField ref="appPassword" | |||
:label="t('settings', 'Password')" | |||
:value="appPassword" | |||
readonly /> | |||
<NcButton type="tertiary" | |||
:title="copyPasswordLabel" | |||
:aria-label="copyPasswordLabel" | |||
@click="copyPassword"> | |||
<template #icon> | |||
<NcIconSvgWrapper :path="copyPasswordIcon" /> | |||
</template> | |||
</NcButton> | |||
</div> | |||
<div class="token-dialog__qrcode"> | |||
<NcButton v-if="!showQRCode" @click="showQRCode = true"> | |||
{{ t('settings', 'Show QR code for mobile apps') }} | |||
</NcButton> | |||
<QR v-else :value="qrUrl" /> | |||
</div> | |||
</NcDialog> | |||
</template> | |||
<script lang="ts"> | |||
import type { ITokenResponse } from '../store/authtoken' | |||
import { mdiCheck, mdiContentCopy } from '@mdi/js' | |||
import { showError } from '@nextcloud/dialogs' | |||
import { translate as t } from '@nextcloud/l10n' | |||
import { getRootUrl } from '@nextcloud/router' | |||
import { defineComponent, type PropType } from 'vue' | |||
import QR from '@chenfengyuan/vue-qrcode' | |||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' | |||
import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' | |||
import NcIconSvgWrapper from '@nextcloud/vue/dist/Components/NcIconSvgWrapper.js' | |||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' | |||
import logger from '../logger' | |||
export default defineComponent({ | |||
name: 'AuthTokenSetupDialog', | |||
components: { | |||
NcButton, | |||
NcDialog, | |||
NcIconSvgWrapper, | |||
NcTextField, | |||
QR, | |||
}, | |||
props: { | |||
token: { | |||
type: Object as PropType<ITokenResponse|null>, | |||
required: false, | |||
default: null, | |||
}, | |||
}, | |||
data() { | |||
return { | |||
isNameCopied: false, | |||
isPasswordCopied: false, | |||
showQRCode: false, | |||
} | |||
}, | |||
computed: { | |||
open: { | |||
get() { | |||
return this.token !== null | |||
}, | |||
set(value: boolean) { | |||
if (!value) { | |||
this.$emit('close') | |||
} | |||
}, | |||
}, | |||
copyPasswordIcon() { | |||
return this.isPasswordCopied ? mdiCheck : mdiContentCopy | |||
}, | |||
copyNameIcon() { | |||
return this.isNameCopied ? mdiCheck : mdiContentCopy | |||
}, | |||
appPassword() { | |||
return this.token?.token ?? '' | |||
}, | |||
loginName() { | |||
return this.token?.loginName ?? '' | |||
}, | |||
qrUrl() { | |||
const server = window.location.protocol + '//' + window.location.host + getRootUrl() | |||
return `nc://login/user:${this.loginName}&password:${this.appPassword}&server:${server}` | |||
}, | |||
copyPasswordLabel() { | |||
if (this.isPasswordCopied) { | |||
return t('settings', 'App password copied!') | |||
} | |||
return t('settings', 'Copy app password') | |||
}, | |||
copyLoginNameLabel() { | |||
if (this.isNameCopied) { | |||
return t('settings', 'Login name copied!') | |||
} | |||
return t('settings', 'Copy login name') | |||
}, | |||
}, | |||
watch: { | |||
token() { | |||
// reset showing the QR code on token change | |||
this.showQRCode = false | |||
}, | |||
open() { | |||
if (this.open) { | |||
this.$nextTick(() => { | |||
this.$refs.appPassword!.select() | |||
}) | |||
} | |||
}, | |||
}, | |||
methods: { | |||
t, | |||
async copyPassword() { | |||
try { | |||
await navigator.clipboard.writeText(this.appPassword) | |||
this.isPasswordCopied = true | |||
} catch (e) { | |||
this.isPasswordCopied = false | |||
logger.error(e as Error) | |||
showError(t('settings', 'Could not copy app password. Please copy it manually.')) | |||
} finally { | |||
setTimeout(() => { | |||
this.isPasswordCopied = false | |||
}, 4000) | |||
} | |||
}, | |||
async copyLoginName() { | |||
try { | |||
await navigator.clipboard.writeText(this.loginName) | |||
this.isNameCopied = true | |||
} catch (e) { | |||
this.isNameCopied = false | |||
logger.error(e as Error) | |||
showError(t('settings', 'Could not copy login name. Please copy it manually.')) | |||
} finally { | |||
setTimeout(() => { | |||
this.isNameCopied = false | |||
}, 4000) | |||
} | |||
}, | |||
}, | |||
}) | |||
</script> | |||
<style scoped lang="scss"> | |||
:deep(.token-dialog) { | |||
display: flex; | |||
flex-direction: column; | |||
gap: 12px; | |||
padding-inline: 22px; | |||
padding-block-end: 20px; | |||
> * { | |||
box-sizing: border-box; | |||
} | |||
} | |||
.token-dialog { | |||
&__name, &__password { | |||
align-items: end; | |||
display: flex; | |||
gap: 10px; | |||
:deep(input) { | |||
font-family: monospace; | |||
} | |||
} | |||
&__qrcode { | |||
display: flex; | |||
justify-content: center; | |||
} | |||
} | |||
</style> |
@@ -1,239 +0,0 @@ | |||
<!-- | |||
- @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- | |||
- @author 2019 Christoph Wurst <christoph@winzerhof-wurst.at> | |||
- | |||
- @license GNU AGPL version 3 or any later version | |||
- | |||
- 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 <http://www.gnu.org/licenses/>. | |||
--> | |||
<template> | |||
<div v-if="!adding" id="generate-app-token-section" class="row spacing"> | |||
<!-- Port to TextField component when available --> | |||
<NcTextField :value.sync="deviceName" | |||
type="text" | |||
:maxlength="120" | |||
:disabled="loading" | |||
class="app-name-text-field" | |||
:label="t('settings', 'App name')" | |||
:placeholder="t('settings', 'App name')" | |||
@keydown.enter="submit" /> | |||
<NcButton :disabled="loading || deviceName.length === 0" | |||
type="primary" | |||
@click="submit"> | |||
{{ t('settings', 'Create new app password') }} | |||
</NcButton> | |||
</div> | |||
<div v-else class="spacing"> | |||
{{ t('settings', 'Use the credentials below to configure your app or device.') }} | |||
{{ t('settings', 'For security reasons this password will only be shown once.') }} | |||
<div class="app-password-row"> | |||
<label for="app-username" class="app-password-label">{{ t('settings', 'Username') }}</label> | |||
<input id="app-username" | |||
:value="loginName" | |||
type="text" | |||
class="monospaced" | |||
readonly="readonly" | |||
@focus="selectInput"> | |||
</div> | |||
<div class="app-password-row"> | |||
<label for="app-password" class="app-password-label">{{ t('settings', 'Password') }}</label> | |||
<input id="app-password" | |||
ref="appPassword" | |||
:value="appPassword" | |||
type="text" | |||
class="monospaced" | |||
readonly="readonly" | |||
@focus="selectInput"> | |||
<NcButton type="tertiary" | |||
:title="copyTooltipOptions" | |||
:aria-label="copyTooltipOptions" | |||
@click="copyPassword"> | |||
<template #icon> | |||
<Check v-if="copied" :size="20" /> | |||
<ContentCopy v-else :size="20" /> | |||
</template> | |||
</NcButton> | |||
<NcButton @click="reset"> | |||
{{ t('settings', 'Done') }} | |||
</NcButton> | |||
</div> | |||
<div class="app-password-row"> | |||
<span class="app-password-label" /> | |||
<NcButton v-if="!showQR" | |||
@click="showQR = true"> | |||
{{ t('settings', 'Show QR code for mobile apps') }} | |||
</NcButton> | |||
<QR v-else | |||
:value="qrUrl" /> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import QR from '@chenfengyuan/vue-qrcode' | |||
import { confirmPassword } from '@nextcloud/password-confirmation' | |||
import '@nextcloud/password-confirmation/dist/style.css' | |||
import { showError } from '@nextcloud/dialogs' | |||
import { getRootUrl } from '@nextcloud/router' | |||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' | |||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' | |||
import Check from 'vue-material-design-icons/Check.vue' | |||
import ContentCopy from 'vue-material-design-icons/ContentCopy.vue' | |||
export default { | |||
name: 'AuthTokenSetupDialogue', | |||
components: { | |||
Check, | |||
ContentCopy, | |||
NcButton, | |||
QR, | |||
NcTextField, | |||
}, | |||
props: { | |||
add: { | |||
type: Function, | |||
required: true, | |||
}, | |||
}, | |||
data() { | |||
return { | |||
adding: false, | |||
loading: false, | |||
deviceName: '', | |||
appPassword: '', | |||
loginName: '', | |||
copied: false, | |||
showQR: false, | |||
qrUrl: '', | |||
} | |||
}, | |||
computed: { | |||
copyTooltipOptions() { | |||
if (this.copied) { | |||
return t('settings', 'Copied!') | |||
} | |||
return t('settings', 'Copy') | |||
}, | |||
}, | |||
methods: { | |||
selectInput(e) { | |||
e.currentTarget.select() | |||
}, | |||
submit() { | |||
confirmPassword() | |||
.then(() => { | |||
this.loading = true | |||
return this.add(this.deviceName) | |||
}) | |||
.then(token => { | |||
this.adding = true | |||
this.loginName = token.loginName | |||
this.appPassword = token.token | |||
const server = window.location.protocol + '//' + window.location.host + getRootUrl() | |||
this.qrUrl = `nc://login/user:${token.loginName}&password:${token.token}&server:${server}` | |||
this.$nextTick(() => { | |||
this.$refs.appPassword.select() | |||
}) | |||
}) | |||
.catch(err => { | |||
console.error('could not create a new app password', err) | |||
OC.Notification.showTemporary(t('settings', 'Error while creating device token')) | |||
this.reset() | |||
}) | |||
}, | |||
async copyPassword() { | |||
try { | |||
await navigator.clipboard.writeText(this.appPassword) | |||
this.copied = true | |||
} catch (e) { | |||
this.copied = false | |||
console.error(e) | |||
showError(t('settings', 'Could not copy app password. Please copy it manually.')) | |||
} finally { | |||
setTimeout(() => { | |||
this.copied = false | |||
}, 4000) | |||
} | |||
}, | |||
reset() { | |||
this.adding = false | |||
this.loading = false | |||
this.showQR = false | |||
this.qrUrl = '' | |||
this.deviceName = '' | |||
this.appPassword = '' | |||
this.loginName = '' | |||
}, | |||
}, | |||
} | |||
</script> | |||
<style lang="scss" scoped> | |||
.app-password-row { | |||
display: flex; | |||
align-items: center; | |||
flex-wrap: wrap; | |||
margin-top: calc(var(--default-grid-baseline) * 2); | |||
.icon { | |||
background-size: 16px 16px; | |||
display: inline-block; | |||
position: relative; | |||
top: 3px; | |||
margin-left: 5px; | |||
margin-right: 8px; | |||
} | |||
} | |||
.app-password-label { | |||
display: table-cell; | |||
margin-right: 1em; | |||
text-align: left; | |||
vertical-align: middle; | |||
width: 100px; | |||
} | |||
.app-name-text-field { | |||
height: 44px !important; | |||
padding-left: 12px; | |||
margin-right: 12px; | |||
width: 200px; | |||
} | |||
.monospaced { | |||
width: 245px; | |||
font-family: monospace; | |||
} | |||
.button-vue{ | |||
display:inline-block; | |||
margin: 3px 3px 3px 3px; | |||
} | |||
.row { | |||
display: flex; | |||
align-items: center; | |||
} | |||
.spacing { | |||
padding-top: 16px; | |||
} | |||
</style> |
@@ -3,6 +3,7 @@ | |||
* | |||
* @author Christoph Wurst <christoph@winzerhof-wurst.at> | |||
* @author John Molakvoæ <skjnldsv@protonmail.com> | |||
* @author Ferdinand Thiessen <opensource@fthiessen.de> | |||
* | |||
* @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') |
@@ -0,0 +1,214 @@ | |||
/** | |||
* @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de> | |||
* | |||
* @author Ferdinand Thiessen <opensource@fthiessen.de> | |||
* | |||
* @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 <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
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<string, boolean> | |||
} | |||
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<IToken[]>('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<ITokenResponse>(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 | |||
}, | |||
}, | |||
}) |
@@ -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", |