summaryrefslogtreecommitdiffstats
path: root/settings/src
diff options
context:
space:
mode:
authorChristoph Wurst <christoph@winzerhof-wurst.at>2019-02-26 19:43:59 +0100
committerChristoph Wurst <christoph@winzerhof-wurst.at>2019-02-28 17:38:48 +0100
commit4b724751307447cb5153ce4708d6ad9d04a6bff5 (patch)
treeda0e43b8a3e198c3451522ab544a202c3cff4ccc /settings/src
parentfb48abc35ab9d36c6d9eef5fa0ac1fe92bba0f73 (diff)
downloadnextcloud-server-4b724751307447cb5153ce4708d6ad9d04a6bff5.tar.gz
nextcloud-server-4b724751307447cb5153ce4708d6ad9d04a6bff5.zip
Move personal auth token settings to Vue
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at> Always crate OC.Settings, even if not used Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
Diffstat (limited to 'settings/src')
-rw-r--r--settings/src/components/AuthToken.vue272
-rw-r--r--settings/src/components/AuthTokenList.vue134
-rw-r--r--settings/src/components/AuthTokenSection.vue153
-rw-r--r--settings/src/components/AuthTokenSetupDialogue.vue181
-rw-r--r--settings/src/main-admin-security.js4
-rw-r--r--settings/src/main-personal-security.js35
6 files changed, 779 insertions, 0 deletions
diff --git a/settings/src/components/AuthToken.vue b/settings/src/components/AuthToken.vue
new file mode 100644
index 00000000000..a8b1bb14437
--- /dev/null
+++ b/settings/src/components/AuthToken.vue
@@ -0,0 +1,272 @@
+<!--
+ - @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">
+ <td class="client">
+ <div :class="iconName.icon"></div>
+ </td>
+ <td class="token-name">
+ <input v-if="token.canRename && renaming"
+ type="text"
+ ref="input"
+ v-model="newName"
+ @keyup.enter="rename"
+ @blur="cancelRename"
+ @keyup.esc="cancelRename">
+ <span v-else>{{iconName.name}}</span>
+ </td>
+ <td>
+ <span class="last-activity" v-tooltip="lastActivity">{{lastActivityRelative}}</span>
+ </td>
+ <td class="more">
+ <Action v-if="!token.current"
+ :actions="actions"
+ v-bind:open.sync="actionOpen"
+ v-tooltip="{content: t('settings', 'Device settings'), container: 'body'}"
+ tabindex="0"/>
+ </td>
+ </tr>
+</template>
+
+<script>
+ import {Action} from 'nextcloud-vue';
+
+ 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.*$/,
+ // DAVdroid/1.2 (2016/07/03; dav4android; okhttp3) Android/6.0.1
+ davDroid: /DAV(droid|x5)\/([0-9.]+)/,
+ // 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', 'Nextcloud iOS app'),
+ androidClient: t('setting', 'Nextcloud Android app'),
+ iosTalkClient: t('setting', 'Nextcloud Talk for iOS'),
+ androidTalkClient: t('setting', 'Nextcloud Talk for Android'),
+ davDroid: 'DAVdroid',
+ 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',
+ davDroid: 'icon-phone',
+ webPirate: 'icon-link',
+ sailfishBrowser: 'icon-link'
+ };
+
+ export default {
+ name: "AuthToken",
+ components: {
+ Action,
+ },
+ props: {
+ token: {
+ type: Object,
+ required: true,
+ }
+ },
+ computed: {
+ actions () {
+ const actions = [];
+
+ if (this.token.type === 1) {
+ // TODO: add text/longtext with some description
+ actions.push({
+ input: 'checkbox',
+ action: () => this.$emit('toggleScope', this.token, 'filesystem', !this.token.scope.filesystem),
+ model: this.token.scope.filesystem,
+ text: t('settings', 'Allow filesystem access'),
+ });
+ }
+ if (this.token.canRename) {
+ // TODO: add text/longtext with some description
+ actions.push({
+ icon: 'icon-rename',
+ action: () => this.startRename(),
+ text: t('settings', 'Rename'),
+ });
+ }
+ if (this.token.canDelete) {
+ // TODO: add text/longtext with some description
+ actions.push({
+ icon: 'icon-delete',
+ action: () => this.$emit('delete', this.token),
+ text: t('settings', 'Revoke'),
+ });
+ }
+
+ return actions;
+ },
+ 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
+ let matches = this.token.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/);
+
+ let icon = '';
+ if (matches) {
+ 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 (let client in userAgentMap) {
+ if (matches = title.match(userAgentMap[client])) {
+ 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,
+ };
+ },
+ },
+ data () {
+ return {
+ showMore: this.token.canScope || this.token.canDelete,
+ renaming: false,
+ newName: '',
+ actionOpen: false,
+ };
+ },
+ 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;
+ },
+ rename () {
+ this.renaming = false;
+ this.$emit('rename', this.token, this.newName);
+ },
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ 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: 16px;
+ }
+
+ &.token-name {
+ padding: 10px 6px;
+
+ &.token-rename {
+ padding: 0;
+ }
+
+ input {
+ width: 100%;
+ margin: 0;
+ }
+ }
+
+ &.more {
+ @extend %icon;
+ }
+
+ &.client {
+ @extend %icon;
+
+ div {
+ opacity: 0.57;
+ width: 44px;
+ height: 44px;
+ }
+ }
+ }
+</style>
diff --git a/settings/src/components/AuthTokenList.vue b/settings/src/components/AuthTokenList.vue
new file mode 100644
index 00000000000..fe92852921c
--- /dev/null
+++ b/settings/src/components/AuthTokenList.vue
@@ -0,0 +1,134 @@
+<!--
+ - @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>
+ <table id="app-tokens-table" :class="{ 'icon-loading' : loading }">
+ <thead v-if="tokens.length">
+ <tr>
+ <th></th>
+ <th>{{ t('settings', 'Device') }}</th>
+ <th>{{ t('settings', 'Last activity') }}</th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody class="token-list">
+ <AuthToken v-for="token in sortedTokens"
+ :key="token.id"
+ :token="token"
+ @toggleScope="toggleScope"
+ @rename="rename"
+ @delete="onDelete"/>
+ </tbody>
+ </table>
+</template>
+
+<script>
+ import AuthToken from './AuthToken';
+
+ export default {
+ name: 'AuthTokenList',
+ components: {
+ AuthToken
+ },
+ props: {
+ tokens: {
+ type: Array,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ }
+ },
+ computed: {
+ sortedTokens () {
+ return this.tokens.sort((t1, t2) => {
+ var ts1 = parseInt(t1.lastActivity, 10);
+ var ts2 = parseInt(t2.lastActivity, 10);
+ return ts2 - ts1;
+ })
+ }
+ },
+ methods: {
+ toggleScope (token, scope, value) {
+ // Just pass it on
+ this.$emit('toggleScope', 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);
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ table {
+ width: 100%;
+ min-height: 50px;
+ padding-top: 5px;
+ max-width: 580px;
+
+ th {
+ opacity: .5;
+ padding: 10px 10px 10px 0;
+ }
+ }
+
+ .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;
+ }
+ }
+ }
+</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;
+ }
+ }
+</style>
diff --git a/settings/src/components/AuthTokenSection.vue b/settings/src/components/AuthTokenSection.vue
new file mode 100644
index 00000000000..e5be46fba15
--- /dev/null
+++ b/settings/src/components/AuthTokenSection.vue
@@ -0,0 +1,153 @@
+<!--
+ - @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 id="security" class="section">
+ <h2>{{ t('settings', 'Devices & sessions') }}</h2>
+ <p class="settings-hint hidden-when-empty">{{ t('settings', 'Web, desktop and mobile clients currently logged in to your account.') }}</p>
+ <AuthTokenList :tokens="tokens"
+ :loading="loading"
+ @toggleScope="toggleTokenScope"
+ @rename="rename"
+ @delete="deleteToken"/>
+ <AuthTokenSetupDialogue :add="addNewToken" />
+ </div>
+</template>
+
+<script>
+ import Axios from 'nextcloud-axios';
+
+ import AuthTokenList from './AuthTokenList';
+ import AuthTokenSetupDialogue from './AuthTokenSetupDialogue';
+
+ /**
+ * Tap into a promise without losing the value
+ */
+ const tap = cb => val => {
+ cb(val);
+ return val;
+ };
+
+ export default {
+ name: "AuthTokenSection",
+ components: {
+ AuthTokenSetupDialogue,
+ AuthTokenList
+ },
+ data() {
+ return {
+ loading: true,
+ baseUrl: OC.generateUrl('/settings/personal/authtokens'),
+ tokens: [],
+ }
+ },
+ mounted() {
+ Axios.get(this.baseUrl)
+ .then(resp => resp.data)
+ .then(tokens => {
+ console.debug('loaded app tokens', tokens);
+ this.loading = false;
+ this.tokens = tokens;
+ })
+ .catch(err => {
+ OC.Notification.showTemporary(t('core', 'Error while loading browser sessions and device tokens'));
+ console.error('could not load app tokens', err);
+ throw err;
+ });
+ },
+ 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')))
+ .then(tap(data => this.tokens.push(data.deviceToken)))
+ .catch(err => {
+ console.error.bind('could not create app password', err);
+ OC.Notification.showTemporary(t('core', '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('core', '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('core', '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);
+
+ 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('core', 'Error while deleting the token'));
+
+ // Restore
+ this.tokens.push(token);
+ })
+ }
+ }
+ }
+</script>
+
+<style scoped>
+
+</style>
diff --git a/settings/src/components/AuthTokenSetupDialogue.vue b/settings/src/components/AuthTokenSetupDialogue.vue
new file mode 100644
index 00000000000..020f4695c79
--- /dev/null
+++ b/settings/src/components/AuthTokenSetupDialogue.vue
@@ -0,0 +1,181 @@
+<!--
+ - @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">
+ <input v-model="deviceName"
+ type="text"
+ @keydown.enter="submit"
+ :disabled="loading"
+ :placeholder="t('settings', 'App name')">
+ <button class="button"
+ :disabled="loading"
+ @click="submit">{{ t('settings', 'Create new app password') }}
+ </button>
+ </div>
+ <div v-else>
+ {{ 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">
+ <span class="app-password-label">{{ t('settings', 'Username') }}</span>
+ <input :value="loginName"
+ type="text"
+ class="monospaced"
+ readonly="readonly"
+ @focus="selectInput"/>
+ </div>
+ <div class="app-password-row">
+ <span class="app-password-label">{{ t('settings', 'Password') }}</span>
+ <input :value="appPassword"
+ type="text"
+ class="monospaced"
+ ref="appPassword"
+ readonly="readonly"
+ @focus="selectInput"/>
+ <a class="icon icon-clippy"
+ ref="clipboardButton"
+ v-tooltip="copyTooltipOptions"
+ @mouseover="hoveringCopyButton = true"
+ @mouseleave="hoveringCopyButton = false"
+ v-clipboard:copy="appPassword"
+ v-clipboard:success="onCopyPassword"
+ v-clipboard:error="onCopyPasswordFailed"></a>
+ <button class="button"
+ @click="reset">
+ {{ t('settings', 'Done') }}
+ </button>
+ </div>
+ </div>
+</template>
+
+<script>
+ import confirmPassword from 'nextcloud-password-confirmation';
+
+ export default {
+ name: 'AuthTokenSetupDialogue',
+ props: {
+ add: {
+ type: Function,
+ required: true,
+ }
+ },
+ data () {
+ return {
+ adding: false,
+ loading: false,
+ deviceName: '',
+ appPassword: '',
+ loginName: '',
+ passwordCopied: false,
+ hoveringCopyButton: false,
+ }
+ },
+ computed: {
+ copyTooltipOptions() {
+ const base = {
+ hideOnTargetClick: false,
+ trigger: 'manual',
+ };
+
+ if (this.passwordCopied) {
+ return {
+ ...base,
+ content:t('core', 'Copied!'),
+ show: true,
+ }
+ } else {
+ return {
+ ...base,
+ content: t('core', 'Copy'),
+ show: this.hoveringCopyButton,
+ }
+ }
+ }
+ },
+ methods: {
+ selectInput (e) {
+ e.currentTarget.select();
+ },
+ submit: function () {
+ confirmPassword()
+ .then(() => {
+ this.loading = true;
+ return this.add(this.deviceName)
+ })
+ .then(token => {
+ this.adding = true;
+ this.loginName = token.loginName;
+ this.appPassword = token.token;
+ this.$nextTick(() => {
+ this.$refs.appPassword.select();
+ })
+ })
+ .catch(err => {
+ console.error('could not create a new app password', err);
+ OC.Notification.showTemporary(t('core', 'Error while creating device token'));
+
+ this.reset();
+ });
+ },
+ onCopyPassword() {
+ this.passwordCopied = true;
+ this.$refs.clipboardButton.blur();
+ setTimeout(() => this.passwordCopied = false, 3000);
+ },
+ onCopyPasswordFailed() {
+ OC.Notification.showTemporary(t('core', 'Could not copy app password. Please copy it manually.'));
+ },
+ reset () {
+ this.adding = false;
+ this.loading = false;
+ this.deviceName = '';
+ this.appPassword = '';
+ this.loginName = '';
+ }
+ }
+ }
+</script>
+
+<style lang="scss" scoped>
+ .app-password-row {
+ display: table-row;
+
+ .icon {
+ background-size: 16px 16px;
+ display: inline-block;
+ position: relative;
+ top: 3px;
+ margin-left: 5px;
+ margin-right: 8px;
+ }
+
+ }
+
+ .app-password-label {
+ display: table-cell;
+ padding-right: 1em;
+ }
+
+ .monospaced {
+ width: 245px;
+ font-family: monospace;
+ }
+</style>
diff --git a/settings/src/main-admin-security.js b/settings/src/main-admin-security.js
index fcf0038740d..a728c085b43 100644
--- a/settings/src/main-admin-security.js
+++ b/settings/src/main-admin-security.js
@@ -7,6 +7,10 @@ __webpack_nonce__ = btoa(OC.requestToken)
Vue.prototype.t = t;
+// Not used here but required for legacy templates
+window.OC = window.OC || {};
+window.OC.Settings = window.OC.Settings || {};
+
store.replaceState(
OCP.InitialState.loadState('settings', 'mandatory2FAState')
)
diff --git a/settings/src/main-personal-security.js b/settings/src/main-personal-security.js
new file mode 100644
index 00000000000..5b481a843ff
--- /dev/null
+++ b/settings/src/main-personal-security.js
@@ -0,0 +1,35 @@
+/*
+ * @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/>.
+ */
+
+import Vue from 'vue';
+import VueClipboard from 'vue-clipboard2';
+import VTooltip from 'v-tooltip';
+
+import AuthTokenSection from './components/AuthTokenSection';
+
+__webpack_nonce__ = btoa(OC.requestToken);
+
+Vue.use(VueClipboard);
+Vue.use(VTooltip);
+Vue.prototype.t = t;
+
+const View = Vue.extend(AuthTokenSection);
+new View().$mount('#security');