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>
This commit is contained in:
parent
fb48abc35a
commit
4b72475130
39 changed files with 7357 additions and 4811 deletions
8
package-lock.json
generated
8
package-lock.json
generated
|
@ -7375,6 +7375,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/vue-click-outside/-/vue-click-outside-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/vue-click-outside/-/vue-click-outside-1.0.7.tgz",
|
||||||
"integrity": "sha1-zdKxYF48SUR4TheU6uShKg9wC9Y="
|
"integrity": "sha1-zdKxYF48SUR4TheU6uShKg9wC9Y="
|
||||||
},
|
},
|
||||||
|
"vue-clipboard2": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-clipboard2/-/vue-clipboard2-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-6/Y9NJErWb4LNBLMgsJSdKb7KpF6/jqXagvKlYut6VQzQsNj6515FpwH0r5hhmeJMqaPzf1kxAw8L8Qvw/QBJQ==",
|
||||||
|
"requires": {
|
||||||
|
"clipboard": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"vue-hot-reload-api": {
|
"vue-hot-reload-api": {
|
||||||
"version": "2.3.3",
|
"version": "2.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz",
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
"v-tooltip": "^2.0.0-rc.33",
|
"v-tooltip": "^2.0.0-rc.33",
|
||||||
"vue": "^2.6.7",
|
"vue": "^2.6.7",
|
||||||
"vue-click-outside": "^1.0.7",
|
"vue-click-outside": "^1.0.7",
|
||||||
|
"vue-clipboard2": "^0.3.0",
|
||||||
"vue-infinite-loading": "^2.4.3",
|
"vue-infinite-loading": "^2.4.3",
|
||||||
"vue-localstorage": "^0.6.2",
|
"vue-localstorage": "^0.6.2",
|
||||||
"vue-multiselect": "^2.1.3",
|
"vue-multiselect": "^2.1.3",
|
||||||
|
|
|
@ -427,84 +427,6 @@ table.nostyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Devices & sessions access & tokens */
|
|
||||||
#security {
|
|
||||||
table {
|
|
||||||
width: 100%;
|
|
||||||
min-height: 50px;
|
|
||||||
padding-top: 5px;
|
|
||||||
max-width: 580px;
|
|
||||||
th {
|
|
||||||
opacity: .5;
|
|
||||||
padding: 10px 10px 10px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.token-list 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tr > *:nth-child(2) {
|
|
||||||
padding-left: 6px;
|
|
||||||
}
|
|
||||||
tr > *:nth-child(3) {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Two-Factor Authentication (2FA) */
|
/* Two-Factor Authentication (2FA) */
|
||||||
|
|
||||||
#two-factor-auth {
|
#two-factor-auth {
|
||||||
|
@ -523,30 +445,6 @@ table.nostyle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#new-app-login-name,
|
|
||||||
#new-app-password {
|
|
||||||
width: 245px;
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-button {
|
.social-button {
|
||||||
padding-left: 0 !important;
|
padding-left: 0 !important;
|
||||||
margin-left: -10px;
|
margin-left: -10px;
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
/* global Backbone */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Christoph Wurst <christoph@owncloud.com>
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
||||||
* @license AGPL-3.0
|
|
||||||
*
|
|
||||||
* This code is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License, version 3,
|
|
||||||
* as published by the Free Software Foundation.
|
|
||||||
*
|
|
||||||
* 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, version 3,
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function(OC) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
OC.Settings = OC.Settings || {};
|
|
||||||
|
|
||||||
var AuthToken = OC.Backbone.Model.extend({
|
|
||||||
});
|
|
||||||
|
|
||||||
OC.Settings.AuthToken = AuthToken;
|
|
||||||
|
|
||||||
})(OC);
|
|
|
@ -1,52 +0,0 @@
|
||||||
/* global Backbone */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Christoph Wurst <christoph@owncloud.com>
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
||||||
* @license AGPL-3.0
|
|
||||||
*
|
|
||||||
* This code is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License, version 3,
|
|
||||||
* as published by the Free Software Foundation.
|
|
||||||
*
|
|
||||||
* 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, version 3,
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function(OC) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
OC.Settings = OC.Settings || {};
|
|
||||||
|
|
||||||
var AuthTokenCollection = OC.Backbone.Collection.extend({
|
|
||||||
|
|
||||||
model: OC.Settings.AuthToken,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show recently used sessions/devices first
|
|
||||||
*
|
|
||||||
* @param {OC.Settigns.AuthToken} t1
|
|
||||||
* @param {OC.Settigns.AuthToken} t2
|
|
||||||
* @returns {Boolean}
|
|
||||||
*/
|
|
||||||
comparator: function (t1, t2) {
|
|
||||||
var ts1 = parseInt(t1.get('lastActivity'), 10);
|
|
||||||
var ts2 = parseInt(t2.get('lastActivity'), 10);
|
|
||||||
return ts2 - ts1;
|
|
||||||
},
|
|
||||||
|
|
||||||
tokenType: null,
|
|
||||||
|
|
||||||
url: OC.generateUrl('/settings/personal/authtokens')
|
|
||||||
});
|
|
||||||
|
|
||||||
OC.Settings.AuthTokenCollection = AuthTokenCollection;
|
|
||||||
|
|
||||||
})(OC);
|
|
|
@ -1,493 +0,0 @@
|
||||||
/* global Handlebars, moment */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author Christoph Wurst <christoph@owncloud.com>
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
||||||
* @license AGPL-3.0
|
|
||||||
*
|
|
||||||
* This code is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License, version 3,
|
|
||||||
* as published by the Free Software Foundation.
|
|
||||||
*
|
|
||||||
* 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, version 3,
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function (OC, _, $, Handlebars, moment) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
OC.Settings = OC.Settings || {};
|
|
||||||
|
|
||||||
var SubView = OC.Backbone.View.extend({
|
|
||||||
collection: null,
|
|
||||||
|
|
||||||
_template: undefined,
|
|
||||||
|
|
||||||
template: function (data) {
|
|
||||||
data.disconnectText = t('settings', 'Disconnect');
|
|
||||||
data.revokeText = t('settings', 'Revoke');
|
|
||||||
data.settingsTitle = t('settings', 'Device settings');
|
|
||||||
data.allowFSAccess = t('settings', 'Allow filesystem access');
|
|
||||||
data.renameText = t('settings', 'Rename');
|
|
||||||
return OC.Settings.Templates['authtoken'](data);
|
|
||||||
},
|
|
||||||
|
|
||||||
initialize: function (options) {
|
|
||||||
this.collection = options.collection;
|
|
||||||
|
|
||||||
this.on(this.collection, 'change', this.render);
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function () {
|
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
var list = this.$('.token-list');
|
|
||||||
var tokens = this.collection.filter(function (token) {
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
list.html('');
|
|
||||||
|
|
||||||
// Show header only if there are tokens to show
|
|
||||||
this._toggleHeader(tokens.length > 0);
|
|
||||||
|
|
||||||
tokens.forEach(function (token) {
|
|
||||||
var viewData = this._formatViewData(token);
|
|
||||||
var html = _this.template(viewData);
|
|
||||||
var $html = $(html);
|
|
||||||
$html.find('.has-tooltip').tooltip({container: 'body'});
|
|
||||||
list.append($html);
|
|
||||||
}.bind(this));
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleLoading: function (state) {
|
|
||||||
this.$('table').toggleClass('icon-loading', state);
|
|
||||||
},
|
|
||||||
|
|
||||||
_toggleHeader: function (show) {
|
|
||||||
this.$('.hidden-when-empty').toggleClass('hidden', !show);
|
|
||||||
},
|
|
||||||
|
|
||||||
_formatViewData: function (token) {
|
|
||||||
var viewData = token.toJSON();
|
|
||||||
var ts = viewData.lastActivity * 1000;
|
|
||||||
viewData.lastActivity = OC.Util.relativeModifiedDate(ts);
|
|
||||||
viewData.lastActivityTime = OC.Util.formatDate(ts, 'LLL');
|
|
||||||
viewData.canScope = token.get('type') === 1;
|
|
||||||
viewData.showMore = viewData.canScope || viewData.canDelete;
|
|
||||||
|
|
||||||
// preserve title for cases where we format it further
|
|
||||||
viewData.title = viewData.name;
|
|
||||||
|
|
||||||
// pretty format sync client user agent
|
|
||||||
var matches = viewData.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/);
|
|
||||||
|
|
||||||
var 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+)/
|
|
||||||
};
|
|
||||||
var 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'
|
|
||||||
};
|
|
||||||
|
|
||||||
var 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'
|
|
||||||
};
|
|
||||||
|
|
||||||
if (matches) {
|
|
||||||
viewData.name = t('settings', 'Sync client - {os}', {
|
|
||||||
os: matches[1],
|
|
||||||
version: matches[2]
|
|
||||||
});
|
|
||||||
viewData.icon = 'icon-desktop';
|
|
||||||
}
|
|
||||||
for (var client in userAgentMap) {
|
|
||||||
if (matches = viewData.title.match(userAgentMap[client])) {
|
|
||||||
if (matches[2] && matches[1]) { // version number and os
|
|
||||||
viewData.name = nameMap[client] + ' ' + matches[2] + ' - ' + matches[1];
|
|
||||||
}else if (matches[1]) { // only version number
|
|
||||||
viewData.name = nameMap[client] + ' ' + matches[1];
|
|
||||||
} else {
|
|
||||||
viewData.name = nameMap[client];
|
|
||||||
}
|
|
||||||
|
|
||||||
// update title - for easier view
|
|
||||||
viewData.title = viewData.name;
|
|
||||||
|
|
||||||
viewData.icon = iconMap[client];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (viewData.current) {
|
|
||||||
viewData.name = t('settings', 'This session');
|
|
||||||
}
|
|
||||||
return viewData;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var AuthTokenView = OC.Backbone.View.extend({
|
|
||||||
collection: null,
|
|
||||||
|
|
||||||
_view: [],
|
|
||||||
|
|
||||||
_form: undefined,
|
|
||||||
|
|
||||||
_tokenName: undefined,
|
|
||||||
|
|
||||||
_addAppPasswordBtn: undefined,
|
|
||||||
|
|
||||||
_result: undefined,
|
|
||||||
|
|
||||||
_newAppLoginName: undefined,
|
|
||||||
|
|
||||||
_newAppPassword: undefined,
|
|
||||||
|
|
||||||
_newAppId: undefined,
|
|
||||||
|
|
||||||
_hideAppPasswordBtn: undefined,
|
|
||||||
|
|
||||||
_addingToken: false,
|
|
||||||
|
|
||||||
initialize: function (options) {
|
|
||||||
this.collection = options.collection;
|
|
||||||
|
|
||||||
var el = '#security';
|
|
||||||
this._view = new SubView({
|
|
||||||
el: el,
|
|
||||||
collection: this.collection
|
|
||||||
});
|
|
||||||
|
|
||||||
var $el = $(el);
|
|
||||||
$('body').on('click', _.bind(this._hideConfigureToken, this));
|
|
||||||
$el.on('click', '.popovermenu', function(event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
});
|
|
||||||
$el.on('click', 'a.icon-delete', _.bind(this._onDeleteToken, this));
|
|
||||||
$el.on('click', '.icon-more', _.bind(this._onConfigureToken, this));
|
|
||||||
$el.on('change', 'input.filesystem', _.bind(this._onSetTokenScope, this));
|
|
||||||
$el.on('click', '.icon-rename', _.bind(this._onRenameToken, this));
|
|
||||||
$el.on('dblclick', '.token-name > span', _.bind(this._onRenameToken, this));
|
|
||||||
$el.on('keyup', '.token-name > input', _.bind(this._onEnterTokenName, this));
|
|
||||||
|
|
||||||
this._form = $('#app-password-form');
|
|
||||||
this._tokenName = $('#app-password-name');
|
|
||||||
this._addAppPasswordBtn = $('#add-app-password');
|
|
||||||
this._addAppPasswordBtn.click(_.bind(this._addAppPassword, this));
|
|
||||||
this._appPasswordName = $('#app-password-name');
|
|
||||||
this._appPasswordName.on('keypress', function(event) {
|
|
||||||
if (event.which === 13) {
|
|
||||||
this._addAppPassword();
|
|
||||||
}
|
|
||||||
}.bind(this));
|
|
||||||
|
|
||||||
this._result = $('#app-password-result');
|
|
||||||
this._newAppLoginName = $('#new-app-login-name');
|
|
||||||
this._newAppLoginName.on('focus', _.bind(this._onNewTokenLoginNameFocus, this));
|
|
||||||
this._newAppPassword = $('#new-app-password');
|
|
||||||
this._newAppPassword.on('focus', _.bind(this._onNewTokenFocus, this));
|
|
||||||
this._hideAppPasswordBtn = $('#app-password-hide');
|
|
||||||
this._hideAppPasswordBtn.click(_.bind(this._hideToken, this));
|
|
||||||
|
|
||||||
this._result.find('.clipboardButton').tooltip({placement: 'bottom', title: t('core', 'Copy'), trigger: 'hover'});
|
|
||||||
|
|
||||||
// Clipboard!
|
|
||||||
var clipboard = new ClipboardJS('.clipboardButton');
|
|
||||||
clipboard.on('success', function(e) {
|
|
||||||
var $input = $(e.trigger);
|
|
||||||
$input.tooltip('hide')
|
|
||||||
.attr('data-original-title', t('core', 'Copied!'))
|
|
||||||
.tooltip('fixTitle')
|
|
||||||
.tooltip({placement: 'bottom', trigger: 'manual'})
|
|
||||||
.tooltip('show');
|
|
||||||
_.delay(function() {
|
|
||||||
$input.tooltip('hide')
|
|
||||||
.attr('data-original-title', t('core', 'Copy'))
|
|
||||||
.tooltip('fixTitle');
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
clipboard.on('error', function (e) {
|
|
||||||
var $input = $(e.trigger);
|
|
||||||
var actionMsg = '';
|
|
||||||
if (/iPhone|iPad/i.test(navigator.userAgent)) {
|
|
||||||
actionMsg = t('core', 'Not supported!');
|
|
||||||
} else if (/Mac/i.test(navigator.userAgent)) {
|
|
||||||
actionMsg = t('core', 'Press ⌘-C to copy.');
|
|
||||||
} else {
|
|
||||||
actionMsg = t('core', 'Press Ctrl-C to copy.');
|
|
||||||
}
|
|
||||||
|
|
||||||
$input.tooltip('hide')
|
|
||||||
.attr('data-original-title', actionMsg)
|
|
||||||
.tooltip('fixTitle')
|
|
||||||
.tooltip({placement: 'bottom', trigger: 'manual'})
|
|
||||||
.tooltip('show');
|
|
||||||
_.delay(function () {
|
|
||||||
$input.tooltip('hide')
|
|
||||||
.attr('data-original-title', t('core', 'Copy'))
|
|
||||||
.tooltip('fixTitle');
|
|
||||||
}, 3000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function () {
|
|
||||||
this._view.render();
|
|
||||||
this._view.toggleLoading(false);
|
|
||||||
},
|
|
||||||
|
|
||||||
reload: function () {
|
|
||||||
var _this = this;
|
|
||||||
|
|
||||||
this._view.toggleLoading(true);
|
|
||||||
|
|
||||||
var loadingTokens = this.collection.fetch();
|
|
||||||
|
|
||||||
$.when(loadingTokens).done(function () {
|
|
||||||
_this.render();
|
|
||||||
});
|
|
||||||
$.when(loadingTokens).fail(function () {
|
|
||||||
OC.Notification.showTemporary(t('core', 'Error while loading browser sessions and device tokens'));
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_addAppPassword: function () {
|
|
||||||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
|
|
||||||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this._addAppPassword, this));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var _this = this;
|
|
||||||
this._toggleAddingToken(true);
|
|
||||||
|
|
||||||
var deviceName = this._tokenName.val() !== '' ? this._tokenName.val() : new Date();
|
|
||||||
var creatingToken = $.ajax(OC.generateUrl('/settings/personal/authtokens'), {
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
name: deviceName
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$.when(creatingToken).done(function (resp) {
|
|
||||||
// We can delete token we add
|
|
||||||
resp.deviceToken.canDelete = true;
|
|
||||||
_this.collection.add(resp.deviceToken);
|
|
||||||
_this.render();
|
|
||||||
_this._newAppLoginName.val(resp.loginName);
|
|
||||||
_this._newAppPassword.val(resp.token);
|
|
||||||
_this._newAppId = resp.deviceToken.id;
|
|
||||||
_this._toggleFormResult(false);
|
|
||||||
_this._newAppPassword.select();
|
|
||||||
_this._tokenName.val('');
|
|
||||||
});
|
|
||||||
$.when(creatingToken).fail(function () {
|
|
||||||
OC.Notification.showTemporary(t('core', 'Error while creating device token'));
|
|
||||||
});
|
|
||||||
$.when(creatingToken).always(function () {
|
|
||||||
_this._toggleAddingToken(false);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_onNewTokenLoginNameFocus: function () {
|
|
||||||
this._newAppLoginName.select();
|
|
||||||
},
|
|
||||||
|
|
||||||
_onNewTokenFocus: function () {
|
|
||||||
this._newAppPassword.select();
|
|
||||||
},
|
|
||||||
|
|
||||||
_hideToken: function () {
|
|
||||||
this._toggleFormResult(true);
|
|
||||||
},
|
|
||||||
|
|
||||||
_toggleAddingToken: function (state) {
|
|
||||||
this._addingToken = state;
|
|
||||||
this._addAppPasswordBtn.toggleClass('icon-loading-small', state);
|
|
||||||
},
|
|
||||||
|
|
||||||
_onConfigureToken: function (event) {
|
|
||||||
event.stopPropagation();
|
|
||||||
this._hideConfigureToken();
|
|
||||||
var $target = $(event.target);
|
|
||||||
var $row = $target.closest('tr');
|
|
||||||
$row.toggleClass('active');
|
|
||||||
$row.find('.popovermenu').toggleClass('open');
|
|
||||||
var id = $row.data('id');
|
|
||||||
},
|
|
||||||
|
|
||||||
_hideConfigureToken: function() {
|
|
||||||
$('.token-list tr').removeClass('active');
|
|
||||||
$('.token-list tr .popovermenu').removeClass('open');
|
|
||||||
},
|
|
||||||
|
|
||||||
_onDeleteToken: function (event) {
|
|
||||||
var $target = $(event.target);
|
|
||||||
var $row = $target.closest('tr');
|
|
||||||
var id = $row.data('id');
|
|
||||||
|
|
||||||
if (id === this._newAppId) {
|
|
||||||
this._toggleFormResult(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
var token = this.collection.get(id);
|
|
||||||
if (_.isUndefined(token)) {
|
|
||||||
// Ignore event
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var destroyingToken = token.destroy();
|
|
||||||
|
|
||||||
$row.find('.icon-delete').tooltip('hide');
|
|
||||||
|
|
||||||
var _this = this;
|
|
||||||
$.when(destroyingToken).fail(function () {
|
|
||||||
OC.Notification.showTemporary(t('core', 'Error while deleting the token'));
|
|
||||||
});
|
|
||||||
$.when(destroyingToken).always(function () {
|
|
||||||
_this.render();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
_onSetTokenScope: function (event) {
|
|
||||||
var $target = $(event.target);
|
|
||||||
var $row = $target.closest('tr');
|
|
||||||
var id = $row.data('id');
|
|
||||||
|
|
||||||
var token = this.collection.get(id);
|
|
||||||
if (_.isUndefined(token)) {
|
|
||||||
// Ignore event
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var scope = token.get('scope');
|
|
||||||
scope.filesystem = $target.is(":checked");
|
|
||||||
|
|
||||||
token.set('scope', scope);
|
|
||||||
token.save();
|
|
||||||
},
|
|
||||||
|
|
||||||
_onRenameToken: function (event) {
|
|
||||||
var $target = $(event.target);
|
|
||||||
var $row = $target.closest('tr');
|
|
||||||
|
|
||||||
var tokenId = $row.data('id');
|
|
||||||
var token = this.collection.get(tokenId);
|
|
||||||
|
|
||||||
if (_.isUndefined(token) || token.get('current') === true) {
|
|
||||||
// Ignore event
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var $tokenName = $row.find('.token-name');
|
|
||||||
var showTokenNameInput = !$tokenName.hasClass('token-rename'); // if class token-rename present input is already visible.
|
|
||||||
|
|
||||||
this._hideTokenNameInput();
|
|
||||||
|
|
||||||
if (showTokenNameInput) {
|
|
||||||
$tokenName.addClass('token-rename');
|
|
||||||
$tokenName.find('span').addClass('hidden');
|
|
||||||
$tokenName.find('input').removeClass('hidden').val(token.get('name')).focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._hideConfigureToken();
|
|
||||||
},
|
|
||||||
|
|
||||||
_onEnterTokenName: function(event) {
|
|
||||||
var $target = $(event.target);
|
|
||||||
var $row = $target.closest('tr');
|
|
||||||
|
|
||||||
var tokenId = $row.data('id');
|
|
||||||
var token = this.collection.get(tokenId);
|
|
||||||
|
|
||||||
if (_.isUndefined(token) || token.get('current') === true) {
|
|
||||||
// Ignore event
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
token.set('name', $target.context.value);
|
|
||||||
|
|
||||||
var _this = this;
|
|
||||||
$.when(token.save()).always(function () {
|
|
||||||
_this.render();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
this._hideTokenNameInput();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_hideTokenNameInput: function () {
|
|
||||||
var $tokenList = $('.token-list td.token-name');
|
|
||||||
$tokenList.removeClass('token-rename');
|
|
||||||
$tokenList.find('span').removeClass('hidden');
|
|
||||||
$tokenList.find('input').addClass('hidden');
|
|
||||||
},
|
|
||||||
|
|
||||||
_toggleFormResult: function (showForm) {
|
|
||||||
if (showForm) {
|
|
||||||
this._result.slideUp();
|
|
||||||
this._form.slideDown();
|
|
||||||
} else {
|
|
||||||
this._form.slideUp();
|
|
||||||
this._result.slideDown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
OC.Settings.AuthTokenView = AuthTokenView;
|
|
||||||
|
|
||||||
})(OC, _, $, Handlebars, moment);
|
|
|
@ -1,7 +0,0 @@
|
||||||
$(document).ready(function () {
|
|
||||||
var collection = new OC.Settings.AuthTokenCollection();
|
|
||||||
var view = new OC.Settings.AuthTokenView({
|
|
||||||
collection: collection
|
|
||||||
});
|
|
||||||
view.reload();
|
|
||||||
});
|
|
|
@ -1,66 +1,5 @@
|
||||||
(function() {
|
(function() {
|
||||||
var template = Handlebars.template, templates = OC.Settings.Templates = OC.Settings.Templates || {};
|
var template = Handlebars.template, templates = OC.Settings.Templates = OC.Settings.Templates || {};
|
||||||
templates['authtoken'] = template({"1":function(container,depth0,helpers,partials,data) {
|
|
||||||
var helper;
|
|
||||||
|
|
||||||
return " <input class=\"hidden\" type=\"text\" value=\""
|
|
||||||
+ container.escapeExpression(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"name","hash":{},"data":data}) : helper)))
|
|
||||||
+ "\" />\n";
|
|
||||||
},"3":function(container,depth0,helpers,partials,data) {
|
|
||||||
var helper;
|
|
||||||
|
|
||||||
return "<a class=\"icon icon-more has-tooltip\" tabindex=\"0\" title=\""
|
|
||||||
+ container.escapeExpression(((helper = (helper = helpers.settingsTitle || (depth0 != null ? depth0.settingsTitle : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"settingsTitle","hash":{},"data":data}) : helper)))
|
|
||||||
+ "\"/>";
|
|
||||||
},"5":function(container,depth0,helpers,partials,data) {
|
|
||||||
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
|
|
||||||
|
|
||||||
return " <li><span class=\"menuitem\">\n <input class=\"filesystem checkbox\" type=\"checkbox\" id=\""
|
|
||||||
+ alias4(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"id","hash":{},"data":data}) : helper)))
|
|
||||||
+ "_filesystem\" "
|
|
||||||
+ ((stack1 = helpers["if"].call(alias1,((stack1 = (depth0 != null ? depth0.scope : depth0)) != null ? stack1.filesystem : stack1),{"name":"if","hash":{},"fn":container.program(6, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
|
|
||||||
+ " tabindex=\"0\" />\n <label for=\""
|
|
||||||
+ alias4(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"id","hash":{},"data":data}) : helper)))
|
|
||||||
+ "_filesystem\">"
|
|
||||||
+ alias4(((helper = (helper = helpers.allowFSAccess || (depth0 != null ? depth0.allowFSAccess : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"allowFSAccess","hash":{},"data":data}) : helper)))
|
|
||||||
+ "</label><br/>\n </span></li>\n";
|
|
||||||
},"6":function(container,depth0,helpers,partials,data) {
|
|
||||||
return "checked";
|
|
||||||
},"8":function(container,depth0,helpers,partials,data) {
|
|
||||||
var helper;
|
|
||||||
|
|
||||||
return " <li>\n <a class=\"icon icon-rename\" tabindex=\"0\">"
|
|
||||||
+ container.escapeExpression(((helper = (helper = helpers.renameText || (depth0 != null ? depth0.renameText : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"renameText","hash":{},"data":data}) : helper)))
|
|
||||||
+ "</a>\n </li>\n";
|
|
||||||
},"10":function(container,depth0,helpers,partials,data) {
|
|
||||||
var helper;
|
|
||||||
|
|
||||||
return " <li>\n <a class=\"icon icon-delete\" tabindex=\"0\">"
|
|
||||||
+ container.escapeExpression(((helper = (helper = helpers.revokeText || (depth0 != null ? depth0.revokeText : depth0)) != null ? helper : helpers.helperMissing),(typeof helper === "function" ? helper.call(depth0 != null ? depth0 : (container.nullContext || {}),{"name":"revokeText","hash":{},"data":data}) : helper)))
|
|
||||||
+ "</a>\n </li>\n";
|
|
||||||
},"compiler":[7,">= 4.0.0"],"main":function(container,depth0,helpers,partials,data) {
|
|
||||||
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
|
|
||||||
|
|
||||||
return "<tr data-id=\""
|
|
||||||
+ alias4(((helper = (helper = helpers.id || (depth0 != null ? depth0.id : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"id","hash":{},"data":data}) : helper)))
|
|
||||||
+ "\">\n <td class=\"client\">\n <div class=\""
|
|
||||||
+ alias4(((helper = (helper = helpers.icon || (depth0 != null ? depth0.icon : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"icon","hash":{},"data":data}) : helper)))
|
|
||||||
+ "\" />\n </td>\n <td class=\"token-name\">\n <span>"
|
|
||||||
+ alias4(((helper = (helper = helpers.name || (depth0 != null ? depth0.name : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"name","hash":{},"data":data}) : helper)))
|
|
||||||
+ "</span>\n"
|
|
||||||
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.canRename : depth0),{"name":"if","hash":{},"fn":container.program(1, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
|
|
||||||
+ " </td>\n <td>\n <span class=\"last-activity has-tooltip\" title=\""
|
|
||||||
+ alias4(((helper = (helper = helpers.lastActivityTime || (depth0 != null ? depth0.lastActivityTime : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"lastActivityTime","hash":{},"data":data}) : helper)))
|
|
||||||
+ "\">"
|
|
||||||
+ alias4(((helper = (helper = helpers.lastActivity || (depth0 != null ? depth0.lastActivity : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"lastActivity","hash":{},"data":data}) : helper)))
|
|
||||||
+ "</span></td>\n <td class=\"more\">\n "
|
|
||||||
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.showMore : depth0),{"name":"if","hash":{},"fn":container.program(3, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
|
|
||||||
+ "\n <div class=\"popovermenu menu\">\n"
|
|
||||||
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.canScope : depth0),{"name":"if","hash":{},"fn":container.program(5, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
|
|
||||||
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.canRename : depth0),{"name":"if","hash":{},"fn":container.program(8, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
|
|
||||||
+ ((stack1 = helpers["if"].call(alias1,(depth0 != null ? depth0.canDelete : depth0),{"name":"if","hash":{},"fn":container.program(10, data, 0),"inverse":container.noop,"data":data})) != null ? stack1 : "")
|
|
||||||
+ " </div>\n </td>\n</tr>\n";
|
|
||||||
},"useData":true});
|
|
||||||
templates['federationscopemenu'] = template({"1":function(container,depth0,helpers,partials,data) {
|
templates['federationscopemenu'] = template({"1":function(container,depth0,helpers,partials,data) {
|
||||||
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
|
var stack1, helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=helpers.helperMissing, alias3="function", alias4=container.escapeExpression;
|
||||||
|
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
<tr data-id="{{id}}">
|
|
||||||
<td class="client">
|
|
||||||
<div class="{{icon}}" />
|
|
||||||
</td>
|
|
||||||
<td class="token-name">
|
|
||||||
<span>{{name}}</span>
|
|
||||||
{{#if canRename}}
|
|
||||||
<input class="hidden" type="text" value="{{name}}" />
|
|
||||||
{{/if}}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="last-activity has-tooltip" title="{{lastActivityTime}}">{{lastActivity}}</span></td>
|
|
||||||
<td class="more">
|
|
||||||
{{#if showMore}}<a class="icon icon-more has-tooltip" tabindex="0" title="{{settingsTitle}}"/>{{/if}}
|
|
||||||
<div class="popovermenu menu">
|
|
||||||
{{#if canScope}}
|
|
||||||
<li><span class="menuitem">
|
|
||||||
<input class="filesystem checkbox" type="checkbox" id="{{id}}_filesystem" {{#if scope.filesystem}}checked{{/if}} tabindex="0" />
|
|
||||||
<label for="{{id}}_filesystem">{{allowFSAccess}}</label><br/>
|
|
||||||
</span></li>
|
|
||||||
{{/if}}
|
|
||||||
{{#if canRename}}
|
|
||||||
<li>
|
|
||||||
<a class="icon icon-rename" tabindex="0">{{renameText}}</a>
|
|
||||||
</li>
|
|
||||||
{{/if}}
|
|
||||||
{{#if canDelete}}
|
|
||||||
<li>
|
|
||||||
<a class="icon icon-delete" tabindex="0">{{revokeText}}</a>
|
|
||||||
</li>
|
|
||||||
{{/if}}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
File diff suppressed because one or more lines are too long
3249
settings/js/vue-1.js
3249
settings/js/vue-1.js
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6799
settings/js/vue-2.js
6799
settings/js/vue-2.js
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
settings/js/vue-7.js
Normal file
2
settings/js/vue-7.js
Normal file
File diff suppressed because one or more lines are too long
1
settings/js/vue-7.js.map
Normal file
1
settings/js/vue-7.js.map
Normal file
File diff suppressed because one or more lines are too long
2
settings/js/vue-8.js
Normal file
2
settings/js/vue-8.js
Normal file
File diff suppressed because one or more lines are too long
1
settings/js/vue-8.js.map
Normal file
1
settings/js/vue-8.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
389
settings/js/vue-settings-personal-security.js
Normal file
389
settings/js/vue-settings-personal-security.js
Normal file
File diff suppressed because one or more lines are too long
1
settings/js/vue-settings-personal-security.js.map
Normal file
1
settings/js/vue-settings-personal-security.js.map
Normal file
File diff suppressed because one or more lines are too long
272
settings/src/components/AuthToken.vue
Normal file
272
settings/src/components/AuthToken.vue
Normal file
|
@ -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>
|
134
settings/src/components/AuthTokenList.vue
Normal file
134
settings/src/components/AuthTokenList.vue
Normal file
|
@ -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>
|
153
settings/src/components/AuthTokenSection.vue
Normal file
153
settings/src/components/AuthTokenSection.vue
Normal file
|
@ -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>
|
181
settings/src/components/AuthTokenSetupDialogue.vue
Normal file
181
settings/src/components/AuthTokenSetupDialogue.vue
Normal file
|
@ -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>
|
|
@ -7,6 +7,10 @@ __webpack_nonce__ = btoa(OC.requestToken)
|
||||||
|
|
||||||
Vue.prototype.t = t;
|
Vue.prototype.t = t;
|
||||||
|
|
||||||
|
// Not used here but required for legacy templates
|
||||||
|
window.OC = window.OC || {};
|
||||||
|
window.OC.Settings = window.OC.Settings || {};
|
||||||
|
|
||||||
store.replaceState(
|
store.replaceState(
|
||||||
OCP.InitialState.loadState('settings', 'mandatory2FAState')
|
OCP.InitialState.loadState('settings', 'mandatory2FAState')
|
||||||
)
|
)
|
||||||
|
|
35
settings/src/main-personal-security.js
Normal file
35
settings/src/main-personal-security.js
Normal file
|
@ -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');
|
|
@ -22,11 +22,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
script('settings', [
|
script('settings', [
|
||||||
'authtoken',
|
|
||||||
'authtoken_collection',
|
|
||||||
'templates',
|
'templates',
|
||||||
'authtoken_view',
|
'vue-settings-personal-security',
|
||||||
'settings/authtoken-init'
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if($_['passwordChangeSupported']) {
|
if($_['passwordChangeSupported']) {
|
||||||
|
@ -93,39 +90,4 @@ if($_['passwordChangeSupported']) {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="security" class="section">
|
<div id="security" class="section"></div>
|
||||||
<h2><?php p($l->t('Devices & sessions'));?></h2>
|
|
||||||
<p class="settings-hint hidden-when-empty"><?php p($l->t('Web, desktop and mobile clients currently logged in to your account.'));?></p>
|
|
||||||
<table class="icon-loading">
|
|
||||||
<thead class="token-list-header">
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<th><?php p($l->t('Device'));?></th>
|
|
||||||
<th><?php p($l->t('Last activity'));?></th>
|
|
||||||
<th></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody class="token-list">
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div id="app-password-form">
|
|
||||||
<input id="app-password-name" type="text" placeholder="<?php p($l->t('App name')); ?>">
|
|
||||||
<button id="add-app-password" class="button"><?php p($l->t('Create new app password')); ?></button>
|
|
||||||
</div>
|
|
||||||
<div id="app-password-result" class="hidden">
|
|
||||||
<span>
|
|
||||||
<?php p($l->t('Use the credentials below to configure your app or device.')); ?>
|
|
||||||
<?php p($l->t('For security reasons this password will only be shown once.')); ?>
|
|
||||||
</span>
|
|
||||||
<div class="app-password-row">
|
|
||||||
<span class="app-password-label"><?php p($l->t('Username')); ?></span>
|
|
||||||
<input id="new-app-login-name" type="text" readonly="readonly"/>
|
|
||||||
</div>
|
|
||||||
<div class="app-password-row">
|
|
||||||
<span class="app-password-label"><?php p($l->t('Password')); ?></span>
|
|
||||||
<input id="new-app-password" type="text" readonly="readonly"/>
|
|
||||||
<a class="clipboardButton icon icon-clippy" data-clipboard-target="#new-app-password"></a>
|
|
||||||
<button id="app-password-hide" class="button"><?php p($l->t('Done')); ?></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ const { VueLoaderPlugin } = require('vue-loader');
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: {
|
entry: {
|
||||||
'settings-apps-users-management': path.join(__dirname, 'src', 'main-apps-users-management'),
|
'settings-apps-users-management': path.join(__dirname, 'src', 'main-apps-users-management'),
|
||||||
'settings-admin-security': path.join(__dirname, 'src', 'main-admin-security')
|
'settings-admin-security': path.join(__dirname, 'src', 'main-admin-security'),
|
||||||
|
'settings-personal-security': path.join(__dirname, 'src', 'main-personal-security')
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
path: path.resolve(__dirname, './js'),
|
path: path.resolve(__dirname, './js'),
|
||||||
|
|
Loading…
Reference in a new issue