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:
Christoph Wurst 2019-02-26 19:43:59 +01:00
parent fb48abc35a
commit 4b72475130
No known key found for this signature in database
GPG key ID: CC42AC2A7F0E56D8
39 changed files with 7357 additions and 4811 deletions

8
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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;

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -1,7 +0,0 @@
$(document).ready(function () {
var collection = new OC.Settings.AuthTokenCollection();
var view = new OC.Settings.AuthTokenView({
collection: collection
});
view.reload();
});

View file

@ -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;

View file

@ -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

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

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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>

View 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>

View 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>

View 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>

View file

@ -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')
) )

View 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');

View file

@ -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>

View file

@ -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'),