diff options
Diffstat (limited to 'apps/settings/src/components/AuthToken.vue')
-rw-r--r-- | apps/settings/src/components/AuthToken.vue | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/apps/settings/src/components/AuthToken.vue b/apps/settings/src/components/AuthToken.vue new file mode 100644 index 00000000000..15286adb135 --- /dev/null +++ b/apps/settings/src/components/AuthToken.vue @@ -0,0 +1,354 @@ +<!-- + - SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <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> + <NcDateTime class="auth-token__last-activity" + :ignore-seconds="true" + :timestamp="tokenLastActivity" /> + </td> + <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="canChangeScope" + :checked="token.scope.filesystem" + @update:checked="updateFileSystemScope"> + <!-- TODO: add text/longtext with some description --> + {{ t('settings', 'Allow filesystem access') }} + </NcActionCheckbox> + <NcActionButton v-if="token.canRename" + icon="icon-rename" + @click.stop.prevent="startRename"> + <!-- TODO: add text/longtext with some description --> + {{ t('settings', 'Rename') }} + </NcActionButton> + + <!-- revoke & wipe --> + <template v-if="token.canDelete"> + <template v-if="token.type !== 2"> + <NcActionButton icon="icon-delete" + @click.stop.prevent="revoke"> + <!-- TODO: add text/longtext with some description --> + {{ t('settings', 'Revoke') }} + </NcActionButton> + <NcActionButton icon="icon-delete" + @click.stop.prevent="wipe"> + {{ t('settings', 'Wipe device') }} + </NcActionButton> + </template> + <NcActionButton v-else-if="token.type === 2" + icon="icon-delete" + :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> + </template> + </NcActions> + </td> + </tr> +</template> + +<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 = { + ie: /(?:MSIE|Trident|Trident\/7.0; rv)[ :](\d+)/, + // Microsoft Edge User Agent from https://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx + edge: /^Mozilla\/5\.0 \([^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/[0-9.]+ (?:Mobile Safari|Safari)\/[0-9.]+ Edge\/[0-9.]+$/, + // Firefox User Agent from https://developer.mozilla.org/en-US/docs/Web/HTTP/Gecko_user_agent_string_reference + firefox: /^Mozilla\/5\.0 \([^)]*(Windows|OS X|Linux)[^)]+\) Gecko\/[0-9.]+ Firefox\/(\d+)(?:\.\d)?$/, + // Chrome User Agent from https://developer.chrome.com/multidevice/user-agent + chrome: /^Mozilla\/5\.0 \([^)]*(Windows|OS X|Linux)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\) Chrome\/(\d+)[0-9.]+ (?:Mobile Safari|Safari)\/[0-9.]+$/, + // Safari User Agent from http://www.useragentstring.com/pages/Safari/ + safari: /^Mozilla\/5\.0 \([^)]*(Windows|OS X)[^)]+\) AppleWebKit\/[0-9.]+ \(KHTML, like Gecko\)(?: Version\/([0-9]+)[0-9.]+)? Safari\/[0-9.A-Z]+$/, + // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent + androidChrome: /Android.*(?:; (.*) Build\/).*Chrome\/(\d+)[0-9.]+/, + iphone: / *CPU +iPhone +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */, + ipad: /\(iPad; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */, + iosClient: /^Mozilla\/5\.0 \(iOS\) (?:ownCloud|Nextcloud)-iOS.*$/, + androidClient: /^Mozilla\/5\.0 \(Android\) (?:ownCloud|Nextcloud)-android.*$/, + iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud-Talk.*$/, + androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud-Talk.*$/, + // DAVx5/3.3.8-beta2-gplay (2021/01/02; dav4jvm; okhttp/4.9.0) Android/10 + davx5: /DAV(?:droid|x5)\/([^ ]+)/, + // Mozilla/5.0 (U; Linux; Maemo; Jolla; Sailfish; like Android 4.3) AppleWebKit/538.1 (KHTML, like Gecko) WebPirate/2.0 like Mobile Safari/538.1 (compatible) + webPirate: /(Sailfish).*WebPirate\/(\d+)/, + // Mozilla/5.0 (Maemo; Linux; U; Jolla; Sailfish; Mobile; rv:31.0) Gecko/31.0 Firefox/31.0 SailfishBrowser/1.0 + sailfishBrowser: /(Sailfish).*SailfishBrowser\/(\d+)/, + // Neon 1.0.0+1 + neon: /Neon \d+\.\d+\.\d+\+\d+/, +} +const nameMap = { + 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', +} + +export default defineComponent({ + name: 'AuthToken', + components: { + NcActions, + NcActionButton, + NcActionCheckbox, + NcButton, + NcDateTime, + NcIconSvgWrapper, + NcTextField, + }, + props: { + token: { + type: Object as PropType<IToken>, + required: true, + }, + }, + setup() { + const authTokenStore = useAuthTokenStore() + return { authTokenStore } + }, + data() { + return { + actionOpen: false, + renaming: false, + newName: '', + oldName: '', + mdiCheck, + } + }, + computed: { + canChangeScope() { + return this.token.type === TokenType.PERMANENT_TOKEN + }, + /** + * 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+)/) + + if (matches) { + return { + id: 'syncClient', + os: matches[1], + version: matches[2], + } + } + + for (const client in userAgentMap) { + const matches = this.token.name.match(userAgentMap[client]) + if (matches) { + return { + id: client, + os: matches[2] && matches[1], + version: matches[2] ?? matches[1], + } + } + } + + 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) { + return t('settings', 'This session') + } + if (this.client === null) { + return this.token.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 === TokenType.WIPING_TOKEN + }, + }, + methods: { + t, + updateFileSystemScope(state: boolean) { + this.authTokenStore.setTokenScope(this.token, 'filesystem', state) + }, + startRename() { + // Close action (popover menu) + this.actionOpen = false + + this.oldName = this.token.name + this.newName = this.token.name + this.renaming = true + this.$nextTick(() => { + this.$refs.input!.select() + }) + }, + cancelRename() { + this.renaming = false + }, + revoke() { + this.actionOpen = false + this.authTokenStore.deleteToken(this.token) + }, + rename() { + this.renaming = false + this.authTokenStore.renameToken(this.token, this.newName) + }, + wipe() { + this.actionOpen = false + this.authTokenStore.wipeToken(this.token) + }, + }, +}) +</script> + +<style lang="scss" scoped> +.auth-token { + border-top: 2px solid var(--color-border); + max-width: 200px; + white-space: normal; + vertical-align: middle; + position: relative; + + &--wiping { + background-color: var(--color-background-dark); + } + + &__name { + padding-block: 10px; + display: flex; + align-items: center; + gap: 6px; + min-width: 355px; // ensure no jumping when renaming + } + + &__name-wrapper { + display: flex; + flex-direction: column; + } + + &__name-form { + align-items: end; + display: flex; + gap: 4px; + } + + &__actions { + padding: 0 10px; + } + + &__last-activity { + padding-inline-start: 10px; + } + + .wiping-warning { + color: var(--color-text-maxcontrast); + } +} +</style> |