server/apps/settings/src/components/AuthToken.vue

308 lines
9.1 KiB
Vue

<!--
- @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>
<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"
@blur="cancelRename"
@keyup.esc="cancelRename">
<span v-else>{{ iconName.name }}</span>
<span v-if="wiping" class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span>
</td>
<td>
<span v-tooltip="lastActivity" class="last-activity">{{ lastActivityRelative }}</span>
</td>
<td class="more">
<Actions v-if="!token.current"
v-tooltip.auto="{
content: t('settings', 'Device settings'),
container: 'body'
}"
:open.sync="actionOpen">
<ActionCheckbox v-if="token.type === 1"
:checked="token.scope.filesystem"
@change.stop.prevent="$emit('toggle-scope', token, 'filesystem', !token.scope.filesystem)">
<!-- TODO: add text/longtext with some description -->
{{ t('settings', 'Allow filesystem access') }}
</ActionCheckbox>
<ActionButton v-if="token.canRename"
icon="icon-rename"
@click.stop.prevent="startRename">
<!-- TODO: add text/longtext with some description -->
{{ t('settings', 'Rename') }}
</ActionButton>
<!-- revoke & wipe -->
<template v-if="token.canDelete">
<template v-if="token.type !== 2">
<ActionButton icon="icon-delete"
@click.stop.prevent="revoke">
<!-- TODO: add text/longtext with some description -->
{{ t('settings', 'Revoke') }}
</ActionButton>
<ActionButton icon="icon-delete"
@click.stop.prevent="wipe">
{{ t('settings', 'Wipe device') }}
</ActionButton>
</template>
<ActionButton v-else-if="token.type === 2"
icon="icon-delete"
:title="t('settings', 'Revoke')"
@click.stop.prevent="revoke">
{{ t('settings', 'Revoking this token might prevent the wiping of your device if it hasn\'t started the wipe yet.') }}
</ActionButton>
</template>
</Actions>
</td>
</tr>
</template>
<script>
import {
Actions,
ActionButton,
ActionCheckbox,
} from '@nextcloud/vue'
// 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-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+)/,
}
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 }),
davx5: 'DAVx5',
webPirate: 'WebPirate',
sailfishBrowser: 'SailfishBrowser',
}
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 {
name: 'AuthToken',
components: {
Actions,
ActionButton,
ActionCheckbox,
},
props: {
token: {
type: Object,
required: true,
},
},
data() {
return {
showMore: this.token.canScope || this.token.canDelete,
renaming: false,
newName: '',
actionOpen: false,
}
},
computed: {
lastActivityRelative() {
return OC.Util.relativeModifiedDate(this.token.lastActivity * 1000)
},
lastActivity() {
return OC.Util.formatDate(this.token.lastActivity * 1000, 'LLL')
},
iconName() {
// 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}', {
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])
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]
}
icon = iconMap[client]
}
}
if (this.token.current) {
name = t('settings', 'This session')
}
return {
icon,
name,
}
},
wiping() {
return this.token.type === 2
},
},
methods: {
startRename() {
// Close action (popover menu)
this.actionOpen = false
this.newName = this.token.name
this.renaming = true
this.$nextTick(() => {
this.$refs.input.select()
})
},
cancelRename() {
this.renaming = false
},
revoke() {
this.actionOpen = false
this.$emit('delete', this.token)
},
rename() {
this.renaming = false
this.$emit('rename', this.token, this.newName)
},
wipe() {
this.actionOpen = false
this.$emit('wipe', 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;
&%icon {
overflow: visible;
position: relative;
width: 44px;
height: 44px;
}
&.token-name {
padding: 10px 6px;
&.token-rename {
padding: 0;
}
input {
width: 100%;
margin: 0;
}
}
&.token-name .wiping-warning {
color: var(--color-text-lighter);
}
&.more {
@extend %icon;
padding: 0 10px;
}
&.client {
@extend %icon;
div {
opacity: 0.57;
width: 44px;
height: 44px;
}
}
}
</style>