aboutsummaryrefslogtreecommitdiffstats
path: root/apps/settings/src/components/AuthToken.vue
diff options
context:
space:
mode:
Diffstat (limited to 'apps/settings/src/components/AuthToken.vue')
-rw-r--r--apps/settings/src/components/AuthToken.vue349
1 files changed, 196 insertions, 153 deletions
diff --git a/apps/settings/src/components/AuthToken.vue b/apps/settings/src/components/AuthToken.vue
index d7ed81a35ab..15286adb135 100644
--- a/apps/settings/src/components/AuthToken.vue
+++ b/apps/settings/src/components/AuthToken.vue
@@ -1,52 +1,46 @@
<!--
- - @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/>.
- -->
+ - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ - SPDX-License-Identifier: AGPL-3.0-or-later
+-->
<template>
- <tr :data-id="token.id"
- :class="wiping">
- <td class="client">
- <div :class="iconName.icon" />
- </td>
- <td class="token-name">
- <input v-if="token.canRename && renaming"
- ref="input"
- v-model="newName"
- type="text"
- @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>
@@ -72,7 +66,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>
@@ -82,12 +76,22 @@
</tr>
</template>
-<script>
-import {
- NcActions,
- NcActionButton,
- NcActionCheckbox,
-} from '@nextcloud/vue'
+<script lang="ts">
+import type { PropType } from 'vue'
+import type { IToken } from '../store/authtoken'
+
+import { mdiCheck, mdiCellphone, mdiTablet, mdiMonitor, mdiWeb, mdiKeyOutline, 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/components/NcActions'
+import NcActionButton from '@nextcloud/vue/components/NcActionButton'
+import NcActionCheckbox from '@nextcloud/vue/components/NcActionCheckbox'
+import NcButton from '@nextcloud/vue/components/NcButton'
+import NcDateTime from '@nextcloud/vue/components/NcDateTime'
+import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
+import NcTextField from '@nextcloud/vue/components/NcTextField'
// When using capture groups the following parts are extracted the first is used as the version number, the second as the OS
const userAgentMap = {
@@ -118,115 +122,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 agent used by the token
+ * This either returns 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 mdiKeyOutline
+ }
+
+ 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
@@ -235,77 +286,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>