Migrate to Vue
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
This commit is contained in:
Родитель
4da04fd450
Коммит
0bb0a8e61b
|
@ -1,5 +1,5 @@
|
|||
/build
|
||||
/js/build
|
||||
/js
|
||||
|
||||
/coverage
|
||||
/node_modules
|
||||
|
|
|
@ -8,41 +8,7 @@
|
|||
* @copyright Christoph Wurst 2016
|
||||
*/
|
||||
|
||||
.u2f-loading {
|
||||
display: inline-block;
|
||||
vertical-align: sub;
|
||||
margin-left: -2px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
/** icons for personal page settings **/
|
||||
.nav-icon-u2f-second-factor-auth, .icon-u2f-device {
|
||||
background-image: url('../img/app-dark.svg?v=1');
|
||||
}
|
||||
|
||||
.u2f-device {
|
||||
line-height: 300%;
|
||||
display: flex;
|
||||
}
|
||||
.u2f-device .more {
|
||||
position: relative;
|
||||
}
|
||||
.u2f-device .more .icon-more {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding-left: 20px;
|
||||
vertical-align: middle;
|
||||
opacity: .7;
|
||||
}
|
||||
.u2f-device .popovermenu {
|
||||
right: -5px;
|
||||
top: 42px;
|
||||
}
|
||||
|
||||
.icon-u2f-device {
|
||||
display: inline-block;
|
||||
background-size: 100%;
|
||||
padding: 3px;
|
||||
margin: 3px;
|
||||
}
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
/* global OCA */
|
||||
|
||||
define(function (require) {
|
||||
'use strict';
|
||||
|
||||
var $ = require('jquery');
|
||||
var u2f = require('u2f-api');
|
||||
|
||||
function toggleError(state) {
|
||||
var $info = $('#u2f-info');
|
||||
var $error = $('#u2f-error');
|
||||
if (state) {
|
||||
$info.hide();
|
||||
$error.show();
|
||||
} else {
|
||||
$info.show();
|
||||
$error.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function signCallback(data) {
|
||||
var $form = $('#u2f-form');
|
||||
var $auth = $('#challenge');
|
||||
if (data.errorCode) {
|
||||
console.error('U2F auth failed: ' + data.errorCode);
|
||||
|
||||
toggleError(true);
|
||||
setTimeout(sign, 5 * 1000);
|
||||
return;
|
||||
}
|
||||
$auth.val(JSON.stringify(data));
|
||||
$form.submit();
|
||||
}
|
||||
|
||||
function checkHTTPS() {
|
||||
if (document.location.protocol !== 'https:') {
|
||||
$('#u2f-http-warning').show();
|
||||
}
|
||||
}
|
||||
|
||||
function sign() {
|
||||
checkHTTPS();
|
||||
var req = JSON.parse($('#u2f-auth').val());
|
||||
|
||||
toggleError(false);
|
||||
u2f.sign(req).then(signCallback).catch(console.error.bind(this));
|
||||
}
|
||||
|
||||
return {
|
||||
sign: sign
|
||||
};
|
||||
});
|
|
@ -1,9 +0,0 @@
|
|||
define(function(require) {
|
||||
'use strict';
|
||||
|
||||
var $ = require('jquery');
|
||||
|
||||
var challenge = require('./challenge');
|
||||
|
||||
$(challenge.sign);
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
define(function () {
|
||||
'use strict';
|
||||
|
||||
var $ = require('jquery');
|
||||
|
||||
var SettingsView = require('./settingsview');
|
||||
|
||||
$(function () {
|
||||
var view = new SettingsView({
|
||||
el: $('#twofactor-u2f-settings')
|
||||
});
|
||||
view.load();
|
||||
});
|
||||
});
|
|
@ -1,14 +0,0 @@
|
|||
/**
|
||||
* Mail
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3 or
|
||||
* later. See the COPYING file.
|
||||
*
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @copyright Christoph Wurst 2015
|
||||
*/
|
||||
|
||||
define(function() {
|
||||
'use strict';
|
||||
return $;
|
||||
});
|
|
@ -1,352 +0,0 @@
|
|||
/* OC, t, Handlebars */
|
||||
|
||||
define(function () {
|
||||
'use strict';
|
||||
|
||||
var _ = require('underscore');
|
||||
var $ = require('jquery');
|
||||
var Backbone = require('backbone');
|
||||
var u2f = require('u2f-api');
|
||||
|
||||
var TEMPLATE = ''
|
||||
+ '<div>'
|
||||
+ ' {{#unless loading}}'
|
||||
+ ' <div>'
|
||||
+ ' {{#unless devices.length}}'
|
||||
+ ' <p>' + t('twofactor_u2f', 'No U2F devices configured. You are not using U2F as second factor at the moment.') + '</p>'
|
||||
+ ' {{else}}'
|
||||
+ ' <p>' + t('twofactor_u2f', 'The following devices are configured for U2F second-factor authentication:') + '</p>'
|
||||
+ ' {{/unless}}'
|
||||
+ ' {{#each devices}}'
|
||||
+ ' <div class="u2f-device" data-u2f-id="{{id}}">'
|
||||
+ ' <span class="icon-u2f-device"></span>'
|
||||
+ ' <span>{{#if name}}{{name}}{{else}}' + t('twofactor_u2f', 'Unnamed device') + '{{/if}}</span>'
|
||||
+ ' <span class="more">'
|
||||
+ ' <a class="icon icon-more"></a>'
|
||||
+ ' <div class="popovermenu">'
|
||||
+ ' <ul>'
|
||||
+ ' <li class="remove-device">'
|
||||
+ ' <a><span class="icon-delete"></span><span>' + t('twofactor_u2f', 'Remove') + '</span></a>'
|
||||
+ ' </li>'
|
||||
+ ' </ul>'
|
||||
+ ' </div>'
|
||||
+ ' </span>'
|
||||
+ ' </div>'
|
||||
+ ' {{/each}}'
|
||||
+ ' </div>'
|
||||
+ ' <input id="u2f-device-name" type="text" placeholder="Name your device">'
|
||||
+ ' <button id="add-u2f-device">' + t('twofactor_u2f', 'Add U2F device') + '</button><br>'
|
||||
+ ' <p><em>' + t('twofactor_u2f', 'You can add as many devices as you like. It is recommended to give each device a distinct name.') + '</em></p>'
|
||||
+ ' {{else}}'
|
||||
+ ' <span class="icon-loading-small u2f-loading"></span>'
|
||||
+ ' <span>' + t('twofactor_u2f', 'Adding a new device …') + '</span>'
|
||||
+ ' {{/unless}}'
|
||||
+ '</div>';
|
||||
|
||||
/**
|
||||
* @class
|
||||
*/
|
||||
var SettingsView = Backbone.View.extend(/** @lends Backbone.View */ {
|
||||
|
||||
/**
|
||||
* @type {function|undefined}
|
||||
*/
|
||||
_template: undefined,
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
_loading: false,
|
||||
|
||||
/**
|
||||
* @type {Object[]}
|
||||
*/
|
||||
_devices: undefined,
|
||||
|
||||
/**
|
||||
* @param {object} data
|
||||
* @returns {string}
|
||||
*/
|
||||
template: function (data) {
|
||||
if (!this._template) {
|
||||
console.log('compiling u2f settings template');
|
||||
this._template = Handlebars.compile(TEMPLATE);
|
||||
}
|
||||
return this._template(data);
|
||||
},
|
||||
|
||||
events: {
|
||||
'click #add-u2f-device': '_onAddU2FDevice',
|
||||
'keydown #u2f-device-name': '_onInputKeyDown',
|
||||
'click .u2f-device .remove-device': '_onRemoveDevice'
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {undefined}
|
||||
*/
|
||||
render: function () {
|
||||
console.log('rendering u2f settings view');
|
||||
this._checkHTTPS();
|
||||
|
||||
this._devices = _.sortBy(this._devices, function (device) {
|
||||
// Underscore's stable sort requires a value for each item
|
||||
return device.name || '';
|
||||
});
|
||||
|
||||
this.$el.html(this.template({
|
||||
loading: this._loading,
|
||||
devices: this._devices
|
||||
}));
|
||||
|
||||
_.each(this._devices, function (device) {
|
||||
var $deviceEl = this.$('div[data-u2f-id="' + device.id + '"]');
|
||||
OC.registerMenu($deviceEl.find('a.icon-more'), $deviceEl.find('.popovermenu'));
|
||||
}, this);
|
||||
|
||||
console.log('u2f settings view rendered');
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
_checkHTTPS: function () {
|
||||
if (document.location.protocol !== 'https:') {
|
||||
console.error('u2f requires https');
|
||||
$('#u2f-http-warning').show();
|
||||
} else {
|
||||
console.log('https connection detected');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_getServerState: function () {
|
||||
var url = OC.generateUrl('/apps/twofactor_u2f/settings/state');
|
||||
return Promise.resolve($.ajax(url, {
|
||||
method: 'GET'
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* @returns {Promise}
|
||||
*/
|
||||
load: function () {
|
||||
return this._getServerState().then(function (data) {
|
||||
this._devices = data.devices;
|
||||
this.render();
|
||||
}.bind(this), function () {
|
||||
console.error('Could not load list of u2f devices');
|
||||
OC.Notification.showTemporary('Could not load list of U2F devices.');
|
||||
}).catch(console.error.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onAddU2FDevice: function () {
|
||||
if (this._loading) {
|
||||
console.log('view is loading, ignoring `_onAddU2FDevice` call');
|
||||
// Ignore event
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this._onRegister();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Event} e
|
||||
*/
|
||||
_onInputKeyDown: function (e) {
|
||||
if (e.which === 13) {
|
||||
return this._onAddU2FDevice();
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onRemoveDevice: function (e) {
|
||||
var deviceId = $(e.target).closest('.u2f-device').data('u2f-id');
|
||||
var device = _.find(this._devices, function (device) {
|
||||
return device.id === deviceId;
|
||||
}, this);
|
||||
if (!device) {
|
||||
console.error('Cannot remove u2f device: unkown');
|
||||
return Promise.reject('Unknown u2f device');
|
||||
}
|
||||
|
||||
return this._requirePasswordConfirmation().then(function () {
|
||||
// Remove visually
|
||||
this._devices.splice(this._devices.indexOf(device), 1);
|
||||
this.render();
|
||||
|
||||
// Remove on server
|
||||
return this._removeOnServer(device);
|
||||
}.bind(this)).catch(function (e) {
|
||||
this._devices.push(device);
|
||||
this.render();
|
||||
console.error(e);
|
||||
OC.Notification.showTemporary(t('twofactor_u2f', 'Could not remove your U2F device'));
|
||||
throw new Error('Could not remove u2f device on server');
|
||||
}.bind(this)).catch(function (e) {
|
||||
console.error('Unexpected error while removing the u2f device', e);
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_onRegister: function () {
|
||||
console.log('starting u2f registration');
|
||||
var name = this.$('#u2f-device-name').val();
|
||||
|
||||
// Show loading feedback
|
||||
this._loading = true;
|
||||
this.render();
|
||||
|
||||
var self = this;
|
||||
return this._requirePasswordConfirmation()
|
||||
.then(this._startRegistrationOnServer)
|
||||
.then(function (data) {
|
||||
console.log('got server u2f registration data');
|
||||
return self._registerU2fDevice(data.req, data.sigs);
|
||||
})
|
||||
.then(function (data) {
|
||||
console.log('finished client-side u2f registration');
|
||||
data.name = name;
|
||||
return self._finishRegisterOnServer(data);
|
||||
})
|
||||
.then(function (newDevice) {
|
||||
console.log('finished server-side u2f registration');
|
||||
self._devices.push(newDevice);
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.error(e);
|
||||
OC.Notification.showTemporary('Error while registering u2f device: ' + e.message);
|
||||
})
|
||||
.then(function () {
|
||||
console.log('finished u2f registration');
|
||||
self._loading = false;
|
||||
self.render();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_startRegistrationOnServer: function () {
|
||||
var url = OC.generateUrl('/apps/twofactor_u2f/settings/startregister');
|
||||
return Promise.resolve($.ajax(url, {
|
||||
method: 'POST'
|
||||
})).catch(function (e) {
|
||||
console.error(e);
|
||||
throw new Error(t('twofactor_u2f', 'Server error while trying to add U2F device'));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_requirePasswordConfirmation: function () {
|
||||
if (!OC.PasswordConfirmation.requiresPasswordConfirmation()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise(function (resolve) {
|
||||
OC.PasswordConfirmation.requirePasswordConfirmation(resolve);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {object} req
|
||||
* @param {object} sigs
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_registerU2fDevice: function (req, sigs) {
|
||||
$('.utf-register-info').slideDown();
|
||||
return u2f.register([req], sigs).then(function (data) {
|
||||
return new Promise(function (resolve, reject) {
|
||||
if (data.errorCode && data.errorCode !== 0) {
|
||||
$('.utf-register-info').slideUp();
|
||||
|
||||
// https://developers.yubico.com/U2F/Libraries/Client_error_codes.html
|
||||
switch (data.errorCode) {
|
||||
case 4:
|
||||
// 4 - DEVICE_INELIGIBLE
|
||||
reject(new Error(t('twofactor_u2f', 'U2F device is already registered (error code {errorCode})', {
|
||||
errorCode: data.errorCode
|
||||
})));
|
||||
break;
|
||||
case 5:
|
||||
// 5 - TIMEOUT
|
||||
reject(new Error(t('twofactor_u2f', 'U2F device registration timeout reached (error code {errorCode})', {
|
||||
errorCode: data.errorCode
|
||||
})));
|
||||
break;
|
||||
default:
|
||||
// 1 - OTHER_ERROR
|
||||
// 2 - BAD_REQUEST
|
||||
// 3 - CONFIGURATION_UNSUPPORTED
|
||||
reject(new Error(t('twofactor_u2f', 'U2F device registration failed (error code {errorCode})', {
|
||||
errorCode: data.errorCode
|
||||
})));
|
||||
}
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} data
|
||||
* @param {string} data.name device name (specified by the user)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_finishRegisterOnServer: function (data) {
|
||||
var url = OC.generateUrl('/apps/twofactor_u2f/settings/finishregister');
|
||||
return Promise.resolve($.ajax(url, {
|
||||
method: 'POST',
|
||||
data: data
|
||||
})).catch(function (e) {
|
||||
console.error(e);
|
||||
throw new Error(t('twofactor_u2f', 'Server error while trying to complete U2F device registration'));
|
||||
}).then(function (data) {
|
||||
$('.utf-register-info').slideUp();
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @param {Object} device
|
||||
* @returns {Promise}
|
||||
*/
|
||||
_removeOnServer: function (device) {
|
||||
var url = OC.generateUrl('/apps/twofactor_u2f/settings/remove');
|
||||
|
||||
return Promise.resolve($.ajax(url, {
|
||||
method: 'POST',
|
||||
data: {
|
||||
id: device.id
|
||||
}
|
||||
})).catch(function (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return SettingsView;
|
||||
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
(function (global) {
|
||||
global.u2f = {};
|
||||
})(window);
|
|
@ -1,138 +0,0 @@
|
|||
/* global expect, Promise, spyOn */
|
||||
|
||||
define(['settingsview'], function(SettingsView) {
|
||||
describe('Settings view', function() {
|
||||
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
jasmine.Ajax.install();
|
||||
view = new SettingsView();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
jasmine.Ajax.uninstall();
|
||||
});
|
||||
|
||||
it('fetches state from the server', function() {
|
||||
spyOn(OC.Notification, 'showTemporary');
|
||||
|
||||
view.load();
|
||||
|
||||
expect(jasmine.Ajax.requests.mostRecent().url).toBe('/apps/twofactor_u2f/settings/state');
|
||||
|
||||
jasmine.Ajax.requests.mostRecent().respondWith({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
responseText: JSON.stringify({
|
||||
enabled: false
|
||||
})
|
||||
});
|
||||
|
||||
expect(OC.Notification.showTemporary).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows a notification if the state cannot be loaded from the server', function(done) {
|
||||
spyOn(OC.Notification, 'showTemporary');
|
||||
|
||||
var loading = view.load();
|
||||
|
||||
expect(jasmine.Ajax.requests.mostRecent().url).toBe('/apps/twofactor_u2f/settings/state');
|
||||
|
||||
jasmine.Ajax.requests.mostRecent().respondWith({
|
||||
status: 500,
|
||||
contentType: 'application/json'
|
||||
});
|
||||
|
||||
loading.then(function() {
|
||||
expect(OC.Notification.showTemporary).toHaveBeenCalled();
|
||||
done();
|
||||
}).catch(function(e) {
|
||||
done.fail(e);
|
||||
});
|
||||
});
|
||||
|
||||
it('asks for password confirmation when the user enables u2f', function(done) {
|
||||
spyOn(OC.Notification, 'showTemporary');
|
||||
spyOn(view, '_getServerState').and.returnValue(Promise.resolve({
|
||||
devices: []
|
||||
}));
|
||||
spyOn(view, '_requirePasswordConfirmation').and.returnValue(Promise.reject({
|
||||
message: 'Wrong password'
|
||||
}));
|
||||
|
||||
view.load().then(function() {
|
||||
view._onAddU2FDevice().then(function() {
|
||||
expect(OC.Notification.showTemporary).toHaveBeenCalledWith('Error while registering u2f device: Wrong password');
|
||||
done();
|
||||
}).catch(function(e) {
|
||||
done.fail(e);
|
||||
});
|
||||
}, function(e) {
|
||||
done.fail(e);
|
||||
});
|
||||
});
|
||||
|
||||
it('lets the user register a new device', function(done) {
|
||||
spyOn(OC.Notification, 'showTemporary');
|
||||
spyOn(view, '_getServerState').and.returnValue(Promise.resolve({
|
||||
devices: []
|
||||
}));
|
||||
spyOn(view, '_registerU2fDevice').and.returnValue(Promise.resolve({}));
|
||||
spyOn(view, '_requirePasswordConfirmation').and.returnValue(Promise.resolve());
|
||||
jasmine.Ajax.stubRequest('/apps/twofactor_u2f/settings/startregister').andReturn({
|
||||
contentType: 'application/json',
|
||||
responseText: JSON.stringify({
|
||||
req: 'reqdata',
|
||||
sigs: 'sigsdata'
|
||||
})
|
||||
});
|
||||
jasmine.Ajax.stubRequest('/apps/twofactor_u2f/settings/finishregister').andReturn({
|
||||
contentType: 'application/json',
|
||||
responseText: JSON.stringify({})
|
||||
});
|
||||
|
||||
view.load().then(function() {
|
||||
expect(view._getServerState).toHaveBeenCalled();
|
||||
return view._onAddU2FDevice().then(function() {
|
||||
expect(view._registerU2fDevice).toHaveBeenCalled();
|
||||
expect(OC.Notification.showTemporary).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
}).catch(function(e) {
|
||||
done.fail(e);
|
||||
});
|
||||
});
|
||||
|
||||
it('lets the user remove a device', function(done) {
|
||||
spyOn(OC.Notification, 'showTemporary');
|
||||
spyOn(view, '_getServerState').and.returnValue(Promise.resolve({
|
||||
devices: [
|
||||
{
|
||||
id: 13,
|
||||
name: 'Yolokey'
|
||||
}
|
||||
]
|
||||
}));
|
||||
spyOn(view, '_requirePasswordConfirmation').and.returnValue(Promise.resolve());
|
||||
jasmine.Ajax.stubRequest('/apps/twofactor_u2f/settings/remove').andReturn({
|
||||
contentType: 'application/json',
|
||||
responseText: JSON.stringify({})
|
||||
});
|
||||
|
||||
view.load().then(function() {
|
||||
expect(view._getServerState).toHaveBeenCalled();
|
||||
var fakeEvent = {
|
||||
target: view.$('.remove-device')
|
||||
};
|
||||
return view._onRemoveDevice(fakeEvent).then(function() {
|
||||
expect(OC.Notification.showTemporary).not.toHaveBeenCalled();
|
||||
done();
|
||||
});
|
||||
}).catch(function(e) {
|
||||
done.fail(e);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
|
@ -1,23 +0,0 @@
|
|||
|
||||
(function (global, Backbone) {
|
||||
// Global variable stubs
|
||||
global.OC = {};
|
||||
global.OC.generateUrl = function (url) {
|
||||
return url;
|
||||
};
|
||||
global.OC.Backbone = Backbone;
|
||||
global.OC.Notification = {};
|
||||
global.OC.Notification.showTemporary = function (txt) {
|
||||
console.error('temporary notification', txt)
|
||||
};
|
||||
global.OC.registerMenu = function () {
|
||||
|
||||
};
|
||||
global.OCA = {};
|
||||
global.t = function (app, txt) {
|
||||
if (app !== 'twofactor_u2f') {
|
||||
throw Error('wrong app used for translatoin');
|
||||
}
|
||||
return txt;
|
||||
};
|
||||
})(window, Backbone);
|
|
@ -1,29 +0,0 @@
|
|||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
module.exports = {
|
||||
entry: {
|
||||
challenge: './js/init_challenge.js',
|
||||
settings: './js/init_settings.js'
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: __dirname + '/build'
|
||||
},
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname), 'node_modules'],
|
||||
alias: {
|
||||
'handlebars': 'handlebars/runtime.js'
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.html$/, loader: "handlebars-loader", query: {
|
||||
extensions: '.html',
|
||||
helperDirs: __dirname + '/templatehelpers'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -1,7 +0,0 @@
|
|||
const merge = require('webpack-merge');
|
||||
const baseConfig = require('./webpack.base.config.js');
|
||||
|
||||
module.exports = merge(baseConfig, {
|
||||
devtool: 'inline-source-map',
|
||||
mode: 'development'
|
||||
});
|
|
@ -1,13 +0,0 @@
|
|||
var webpack = require('webpack');
|
||||
const merge = require('webpack-merge');
|
||||
const baseConfig = require('./webpack.base.config.js');
|
||||
|
||||
module.exports = merge(baseConfig, {
|
||||
plugins: [
|
||||
new webpack.optimize.AggressiveMergingPlugin(), // Merge chunks
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true
|
||||
})
|
||||
],
|
||||
mode: 'production'
|
||||
});
|
|
@ -1,77 +0,0 @@
|
|||
// Karma configuration
|
||||
|
||||
var webpackConfig = require('./js/webpack.dev.config.js');
|
||||
|
||||
webpackConfig.entry = {
|
||||
challenge: './js/challenge.js',
|
||||
settings: './js/settingsview.js'
|
||||
};
|
||||
webpackConfig.module.rules.push({
|
||||
test: /\.js$/,
|
||||
exclude: /^init_/
|
||||
});
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['jasmine-ajax', 'jasmine'],
|
||||
|
||||
files: [
|
||||
{pattern: 'node_modules/es6-promise/dist/es6-promise.auto.js', included: true},
|
||||
{pattern: 'node_modules/jquery/dist/jquery.js', included: true},
|
||||
{pattern: 'node_modules/handlebars/dist/handlebars.js', included: true},
|
||||
{pattern: 'node_modules/underscore/underscore.js', included: true},
|
||||
{pattern: 'node_modules/backbone/backbone.js', included: true},
|
||||
{pattern: 'js/tests/test-main.js', included: true},
|
||||
// all files ending in "_test"
|
||||
{pattern: 'js/tests/spec/*_spec.js', watched: false},
|
||||
{pattern: 'js/build/*.js', included: false}
|
||||
],
|
||||
|
||||
// list of files to exclude
|
||||
exclude: [
|
||||
'js/webpack.*.js',
|
||||
'js/init*.js'
|
||||
],
|
||||
// preprocess matching files before serving them to the browser
|
||||
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
'js/**[!vendor]/*[!spec].js': ['coverage', 'sourcemap'],
|
||||
// add webpack as preprocessor
|
||||
'js/tests/*_spec.js': ['webpack', 'sourcemap'],
|
||||
'js/tests/**/*_spec.js': ['webpack', 'sourcemap']
|
||||
},
|
||||
|
||||
webpackMiddleware: {
|
||||
// webpack-dev-middleware configuration
|
||||
// i. e.
|
||||
stats: 'errors-only'
|
||||
},
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ['progress', 'coverage'],
|
||||
coverageReporter: {
|
||||
type: 'lcov',
|
||||
dir: 'coverage/'
|
||||
},
|
||||
// web server port
|
||||
port: 9876,
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: true,
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ['PhantomJS'],
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: false,
|
||||
webpack: webpackConfig
|
||||
});
|
||||
};
|
|
@ -11,8 +11,6 @@ exclude = [
|
|||
"composer.json",
|
||||
"composer.lock",
|
||||
"composer.phar",
|
||||
"js/tests",
|
||||
"js/*.js",
|
||||
"karma.conf.js",
|
||||
"krankerl.toml",
|
||||
"l10n/no-php",
|
||||
|
@ -22,6 +20,7 @@ exclude = [
|
|||
"package.json",
|
||||
"package-lock.json",
|
||||
"screenshots",
|
||||
"src",
|
||||
"tests",
|
||||
"vendor/bin",
|
||||
]
|
||||
|
|
|
@ -84,7 +84,7 @@ class U2FProvider implements IProvider, IProvidesIcons, IProvidesPersonalSetting
|
|||
}
|
||||
|
||||
public function getPersonalSettings(IUser $user): IPersonalProviderSettings {
|
||||
return new Personal();
|
||||
return new Personal($this->manager->getDevices($user));
|
||||
}
|
||||
|
||||
public function getLightIcon(): String {
|
||||
|
|
|
@ -23,12 +23,22 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\TwoFactorU2F\Settings;
|
||||
|
||||
use function json_encode;
|
||||
use OCP\Authentication\TwoFactorAuth\IPersonalProviderSettings;
|
||||
use OCP\Template;
|
||||
|
||||
class Personal implements IPersonalProviderSettings {
|
||||
|
||||
/** @var array */
|
||||
private $devices;
|
||||
|
||||
public function __construct(array $devices) {
|
||||
$this->devices = $devices;
|
||||
}
|
||||
|
||||
public function getBody(): Template {
|
||||
return new Template('twofactor_u2f', 'personal');
|
||||
$template = new Template('twofactor_u2f', 'personal');
|
||||
$template->assign('state', json_encode($this->devices));
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
64
package.json
64
package.json
|
@ -1,38 +1,52 @@
|
|||
{
|
||||
"name": "twofactor_u2f",
|
||||
"version": "1.6.1",
|
||||
"description": "![Downloads](https://img.shields.io/github/downloads/nextcloud/twofactor_u2f/total.svg) [![Build Status](https://api.travis-ci.org/nextcloud/twofactor_u2f.svg?branch=master)](https://travis-ci.org/nextcloud/twofactor_u2f)",
|
||||
"main": "js/challenge.js",
|
||||
"directories": {
|
||||
"test": "tests"
|
||||
},
|
||||
"description": "U2F second factor provider for Nextcloud",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"es6-promise": "^4.2.5",
|
||||
"handlebars": "^4.0.12",
|
||||
"u2f-api": "^1.0.6"
|
||||
"nextcloud-axios": "^0.1.2",
|
||||
"nextcloud-password-confirmation": "^0.3.1",
|
||||
"nextcloud-server": "^0.15.7",
|
||||
"nextcloud-vue": "^0.2.0",
|
||||
"u2f-api": "^1.0.6",
|
||||
"vue": "^2.5.17",
|
||||
"vue-click-outside": "^1.0.7",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"backbone": "^1.3.3",
|
||||
"jasmine": "^3.2.0",
|
||||
"jasmine-ajax": "^3.4.0",
|
||||
"jasmine-core": "^3.2.1",
|
||||
"jquery": "^3.3.1",
|
||||
"karma": "^3.0.0",
|
||||
"karma-coverage": "^1.1.2",
|
||||
"karma-jasmine": "^1.1.2",
|
||||
"karma-jasmine-ajax": "^0.1.13",
|
||||
"karma-phantomjs-launcher": "^1.0.4",
|
||||
"karma-sourcemap-loader": "^0.3.7",
|
||||
"karma-webpack": "^3.0.5",
|
||||
"underscore": "^1.9.1",
|
||||
"@babel/core": "^7.1.2",
|
||||
"@babel/preset-env": "^7.1.0",
|
||||
"@vue/babel-preset-app": "^3.0.5",
|
||||
"@vue/test-utils": "^1.0.0-beta.25",
|
||||
"babel-loader": "^8.0.4",
|
||||
"chai": "^4.2.0",
|
||||
"css-loader": "^1.0.0",
|
||||
"file-loader": "^2.0.0",
|
||||
"jsdom": "^12.2.0",
|
||||
"jsdom-global": "^3.0.2",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-webpack": "^2.0.0-beta.0",
|
||||
"sinon": "^7.0.0",
|
||||
"url-loader": "^1.1.2",
|
||||
"vue-loader": "^15.4.2",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
"webpack": "^4.20.2",
|
||||
"webpack-cli": "^3.1.2",
|
||||
"webpack-merge": "^4.1.4"
|
||||
"webpack-merge": "^4.1.4",
|
||||
"webpack-node-externals": "^1.7.2"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./node_modules/webpack-cli/bin/cli.js --config js/webpack.prod.config.js",
|
||||
"dev": " ./node_modules/webpack-cli/bin/cli.js --config js/webpack.dev.config.js --watch",
|
||||
"test": "./node_modules/karma/bin/karma start karma.conf.js --single-run"
|
||||
"build": "webpack --progress --config src/webpack.prod.js",
|
||||
"dev": "webpack --progress --watch --config src/webpack.dev.js",
|
||||
"test": "mocha-webpack --webpack-config src/webpack.test.js --require src/tests/setup.js src/tests/**/*.spec.js",
|
||||
"test:watch": "mocha-webpack -w --webpack-config src/webpack.test.js --require src/tests/setup.js src/tests/**/*.spec.js"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie >= 11"
|
||||
],
|
||||
"jshintConfig": {
|
||||
"esversion": 6
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
<!--
|
||||
- @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
-
|
||||
- @author 2018 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="step === RegistrationSteps.READY">
|
||||
<button
|
||||
v-on:click="start">{{ t('twofactor_u2f', 'Add U2F device') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="step === RegistrationSteps.U2F_REGISTRATION"
|
||||
class="new-u2f-device">
|
||||
<span class="icon-loading-small u2f-loading"></span>
|
||||
{{ t('twofactor_u2f', 'Please plug in your U2F device and press the device button to authorize.') }}
|
||||
</div>
|
||||
|
||||
<div v-else-if="step === RegistrationSteps.NAMING"
|
||||
class="new-u2f-device">
|
||||
<span class="icon-loading-small u2f-loading"></span>
|
||||
<input type="text"
|
||||
:placeholder="t('twofactor_u2f', 'Name your device')"
|
||||
v-model="name">
|
||||
<button v-on:click="submit">{{ t('twofactor_u2f', 'Add') }}</button>
|
||||
</div>
|
||||
|
||||
<div v-else-if="step === RegistrationSteps.PERSIST"
|
||||
class="new-u2f-device">
|
||||
<span class="icon-loading-small u2f-loading"></span>
|
||||
{{ t('twofactor_u2f', 'Adding your device …') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
Invalid registration step. This should not have happened.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import confirmPassword from 'nextcloud-password-confirmation'
|
||||
import u2f from 'u2f-api'
|
||||
|
||||
import {
|
||||
startRegistration,
|
||||
finishRegistration
|
||||
} from '../services/RegistrationService'
|
||||
|
||||
const logAndPass = (text) => (data) => {
|
||||
console.debug(text)
|
||||
return data
|
||||
}
|
||||
|
||||
const RegistrationSteps = Object.freeze({
|
||||
READY: 1,
|
||||
U2F_REGISTRATION: 2,
|
||||
NAMING: 3,
|
||||
PERSIST: 4,
|
||||
})
|
||||
|
||||
export default {
|
||||
name: 'AddDeviceDialog',
|
||||
props: {
|
||||
httpWarning: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
registrationData: {},
|
||||
RegistrationSteps,
|
||||
step: RegistrationSteps.READY,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
start () {
|
||||
this.step = RegistrationSteps.U2F_REGISTRATION
|
||||
|
||||
return confirmPassword()
|
||||
.then(this.getRegistrationData)
|
||||
.then(this.register)
|
||||
.then(() => this.step = RegistrationSteps.NAMING)
|
||||
.catch(console.error.bind(this))
|
||||
},
|
||||
|
||||
getRegistrationData () {
|
||||
return startRegistration()
|
||||
.catch(err => {
|
||||
console.error('Error getting u2f registration data from server', err)
|
||||
throw new Error(t('twofactor_u2f', 'Server error while trying to add U2F device'))
|
||||
})
|
||||
},
|
||||
|
||||
register ({req, sigs}) {
|
||||
console.debug('starting u2f registration')
|
||||
|
||||
return u2f.register([req], sigs)
|
||||
.then(data => {
|
||||
console.debug('u2f registration was successful', data)
|
||||
|
||||
if (data.errorCode && data.errorCode !== 0) {
|
||||
return this.rejectRegistration(data)
|
||||
}
|
||||
|
||||
this.registrationData = data
|
||||
})
|
||||
},
|
||||
|
||||
rejectRegistration (data) {
|
||||
// https://developers.yubico.com/U2F/Libraries/Client_error_codes.html
|
||||
switch (data.errorCode) {
|
||||
case 4:
|
||||
// 4 - DEVICE_INELIGIBLE
|
||||
Promise.reject(new Error(t('twofactor_u2f', 'U2F device is already registered (error code {errorCode})', {
|
||||
errorCode: data.errorCode
|
||||
})));
|
||||
break;
|
||||
case 5:
|
||||
// 5 - TIMEOUT
|
||||
Promise.reject(new Error(t('twofactor_u2f', 'U2F device registration timeout reached (error code {errorCode})', {
|
||||
errorCode: data.errorCode
|
||||
})));
|
||||
break;
|
||||
default:
|
||||
// 1 - OTHER_ERROR
|
||||
// 2 - BAD_REQUEST
|
||||
// 3 - CONFIGURATION_UNSUPPORTED
|
||||
Promise.reject(new Error(t('twofactor_u2f', 'U2F device registration failed (error code {errorCode})', {
|
||||
errorCode: data.errorCode
|
||||
})));
|
||||
}
|
||||
},
|
||||
|
||||
submit () {
|
||||
this.step = RegistrationSteps.PERSIST
|
||||
|
||||
return confirmPassword()
|
||||
.then(logAndPass('confirmed password'))
|
||||
.then(this.saveRegistrationData)
|
||||
.then(logAndPass('registration data saved'))
|
||||
.then(() => this.reset())
|
||||
.then(logAndPass('app reset'))
|
||||
.catch(console.error.bind(this))
|
||||
},
|
||||
|
||||
saveRegistrationData () {
|
||||
const data = this.registrationData
|
||||
data.name = this.name
|
||||
|
||||
return finishRegistration(data)
|
||||
.then(device => this.$store.commit('addDevice', device))
|
||||
.then(logAndPass('new device added to store'))
|
||||
.catch(err => {
|
||||
console.error('Error persisting u2f registration', err)
|
||||
throw new Error(t('twofactor_u2f', 'Server error while trying to complete U2F device registration'))
|
||||
})
|
||||
},
|
||||
|
||||
reset () {
|
||||
this.name = ''
|
||||
this.registrationData = {}
|
||||
this.step = RegistrationSteps.READY
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.u2f-loading {
|
||||
display: inline-block;
|
||||
vertical-align: sub;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.new-u2f-device {
|
||||
line-height: 300%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,112 @@
|
|||
<!--
|
||||
- @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
-
|
||||
- @author 2018 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 class="u2f-device" :data-u2f-id="id">
|
||||
<span class="icon-u2f-device"></span>
|
||||
{{name || t('twofactor_u2f', 'Unnamed device') }}
|
||||
<span class="more">
|
||||
<a class="icon icon-more"
|
||||
v-on:click.stop="togglePopover"></a>
|
||||
<div class="popovermenu"
|
||||
:class="{open: showPopover}"
|
||||
v-click-outside="hidePopover">
|
||||
<PopoverMenu :menu="menu"/>
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
import confirmPassword from 'nextcloud-password-confirmation'
|
||||
import {PopoverMenu} from 'nextcloud-vue'
|
||||
|
||||
export default {
|
||||
name: 'Device',
|
||||
props: {
|
||||
id: Number,
|
||||
name: String,
|
||||
},
|
||||
components: {
|
||||
PopoverMenu
|
||||
},
|
||||
directives: {
|
||||
ClickOutside
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
showPopover: false,
|
||||
menu: [
|
||||
{
|
||||
text: t('twofactor_u2f', 'Remove'),
|
||||
icon: 'icon-delete',
|
||||
action: () => {
|
||||
confirmPassword()
|
||||
.then(() => this.$store.dispatch('removeDevice', this.id))
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
togglePopover () {
|
||||
this.showPopover = !this.showPopover
|
||||
},
|
||||
|
||||
hidePopover () {
|
||||
this.showPopover = false
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.u2f-device {
|
||||
line-height: 300%;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.u2f-device .more {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.u2f-device .more .icon-more {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding-left: 20px;
|
||||
vertical-align: middle;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
.u2f-device .popovermenu {
|
||||
right: -12px;
|
||||
top: 42px;
|
||||
}
|
||||
|
||||
.icon-u2f-device {
|
||||
display: inline-block;
|
||||
background-size: 100%;
|
||||
padding: 3px;
|
||||
margin: 3px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,73 @@
|
|||
<!--
|
||||
- @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
-
|
||||
- @author 2018 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>
|
||||
<p v-if="devices.length === 0">{{ t('twofactor_u2f', 'No U2F devices configured. You are not using U2F as second factor at the moment.') }}</p>
|
||||
<p v-else>{{ t('twofactor_u2f', 'The following devices are configured for U2F second-factor authentication:') }}</p>
|
||||
<Device v-for="device in devices"
|
||||
:key="device.id"
|
||||
:id="device.id"
|
||||
:name="device.name" />
|
||||
|
||||
<AddDeviceDialog :httpWarning="httpWarning" />
|
||||
<p v-if="notSupported">
|
||||
{{ t('twofactor_u2f', 'Your browser does not support u2f.') }}
|
||||
{{ t('twofactor_u2f', 'Chrome is the only browser that supports U2F devices. You need to install the "U2F Support Add-on" on Firefox to use U2F.') }}
|
||||
</p>
|
||||
<p v-if="httpWarning"
|
||||
id="u2f-http-warning">
|
||||
{{ t('twofactor_u2f', 'You are accessing this site via an insecure connection. Browsers might therefore refuse the U2F authentication.') }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import u2f from 'u2f-api'
|
||||
|
||||
import AddDeviceDialog from './AddDeviceDialog'
|
||||
import Device from './Device'
|
||||
|
||||
export default {
|
||||
name: 'PersonalSettings',
|
||||
props: {
|
||||
httpWarning: Boolean
|
||||
},
|
||||
components: {
|
||||
AddDeviceDialog,
|
||||
Device
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
notSupported: !u2f.isSupported()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
devices() {
|
||||
return this.$store.state.devices
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2018 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 store from './store'
|
||||
import Vue from 'vue'
|
||||
|
||||
import Nextcloud from './mixins/Nextcloud'
|
||||
|
||||
Vue.mixin(Nextcloud)
|
||||
|
||||
const initialStateElement = document.getElementById('twofactor-u2f-initial-state')
|
||||
if (initialStateElement) {
|
||||
const devices = JSON.parse(initialStateElement.value)
|
||||
devices.sort((d1, d2) => d1.name.localeCompare(d2.name))
|
||||
store.replaceState({
|
||||
devices
|
||||
})
|
||||
}
|
||||
|
||||
import PersonalSettings from './components/PersonalSettings'
|
||||
|
||||
const View = Vue.extend(PersonalSettings)
|
||||
new View({
|
||||
propsData: {
|
||||
httpWarning: document.location.protocol !== 'https:',
|
||||
},
|
||||
store,
|
||||
}).$mount('#twofactor-u2f-settings')
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2018 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/>.
|
||||
*/
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
t,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2018 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 Axios from 'nextcloud-axios'
|
||||
import {generateUrl} from 'nextcloud-server/dist/router'
|
||||
|
||||
export function startRegistration () {
|
||||
const url = generateUrl('/apps/twofactor_u2f/settings/startregister')
|
||||
|
||||
return Axios.post(url)
|
||||
.then(resp => resp.data)
|
||||
}
|
||||
|
||||
export function finishRegistration (data) {
|
||||
const url = generateUrl('/apps/twofactor_u2f/settings/finishregister')
|
||||
|
||||
return Axios.post(url, data)
|
||||
.then(resp => resp.data)
|
||||
}
|
||||
|
||||
export function removeRegistration (id) {
|
||||
const url = generateUrl('/apps/twofactor_u2f/settings/remove')
|
||||
const data = {
|
||||
id
|
||||
}
|
||||
|
||||
return Axios.post(url, data)
|
||||
.then(resp => resp.data)
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import {removeRegistration} from './services/RegistrationService'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export const mutations = {
|
||||
addDevice (state, device) {
|
||||
state.devices.push(device)
|
||||
state.devices.sort((d1, d2) => d1.name.localeCompare(d2.name))
|
||||
},
|
||||
|
||||
removeDevice (state, id) {
|
||||
state.devices = state.devices.filter(device => device.id !== id)
|
||||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
removeDevice ({state, commit}, id) {
|
||||
const device = state.devices[id]
|
||||
|
||||
commit('removeDevice', id)
|
||||
|
||||
removeRegistration(id)
|
||||
.catch(err => {
|
||||
// Rollback
|
||||
commit('addDevice', device)
|
||||
|
||||
throw err
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const getters = {}
|
||||
|
||||
export default new Vuex.Store({
|
||||
strict: process.env.NODE_ENV !== 'production',
|
||||
state: {
|
||||
devices: []
|
||||
},
|
||||
getters,
|
||||
mutations,
|
||||
actions
|
||||
})
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2018 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 {shallowMount, createLocalVue} from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import Nextcloud from '../../mixins/Nextcloud'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
|
||||
localVue.use(Vuex)
|
||||
localVue.mixin(Nextcloud)
|
||||
|
||||
import Device from '../../components/Device'
|
||||
|
||||
describe('Device component', () => {
|
||||
var actions
|
||||
var store
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {}
|
||||
store = new Vuex.Store({
|
||||
state: {
|
||||
devices: []
|
||||
},
|
||||
actions
|
||||
})
|
||||
})
|
||||
|
||||
it('renders devices without a name', () => {
|
||||
store.state.devices.push({
|
||||
id: 1,
|
||||
name: undefined
|
||||
})
|
||||
const device = shallowMount(Device, {
|
||||
store,
|
||||
localVue
|
||||
})
|
||||
|
||||
expect(device.text()).to.have.string('Unnamed device')
|
||||
})
|
||||
|
||||
it('has a closed popover menu by default', () => {
|
||||
const device = shallowMount(Device, {
|
||||
store,
|
||||
localVue
|
||||
})
|
||||
|
||||
expect(device.contains('.popovermenu.open')).to.be.false
|
||||
})
|
||||
|
||||
it('opens popover menu on click', done => {
|
||||
const device = shallowMount(Device, {
|
||||
store,
|
||||
localVue
|
||||
})
|
||||
|
||||
device.find('.icon-more').trigger('click')
|
||||
|
||||
localVue.nextTick(() => {
|
||||
expect(device.vm.showPopover).to.be.true
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
it('closed popover menu on second click', done => {
|
||||
const device = shallowMount(Device, {
|
||||
store,
|
||||
localVue
|
||||
})
|
||||
device.vm.showPopover = true
|
||||
|
||||
device.find('.icon-more').trigger('click')
|
||||
|
||||
localVue.nextTick(() => {
|
||||
expect(device.vm.showPopover).to.be.false
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2018 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 {shallowMount, createLocalVue} from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
|
||||
import Nextcloud from '../../mixins/Nextcloud'
|
||||
|
||||
const localVue = createLocalVue()
|
||||
|
||||
localVue.use(Vuex)
|
||||
localVue.mixin(Nextcloud)
|
||||
|
||||
import PersonalSettings from '../../components/PersonalSettings'
|
||||
|
||||
describe('Device component', () => {
|
||||
var actions
|
||||
var store
|
||||
|
||||
beforeEach(() => {
|
||||
actions = {}
|
||||
store = new Vuex.Store({
|
||||
state: {
|
||||
devices: []
|
||||
},
|
||||
actions
|
||||
})
|
||||
})
|
||||
|
||||
it('shows text if no devices are configured', () => {
|
||||
const settings = shallowMount(PersonalSettings, {
|
||||
store,
|
||||
localVue
|
||||
})
|
||||
|
||||
expect(settings.text()).to.contain('No U2F devices configured')
|
||||
})
|
||||
|
||||
it('shows no info text if devices are configured', () => {
|
||||
const settings = shallowMount(PersonalSettings, {
|
||||
store,
|
||||
localVue
|
||||
})
|
||||
store.state.devices.push({
|
||||
id: 1,
|
||||
name: 'a'
|
||||
})
|
||||
|
||||
expect(settings.text()).to.not.contain('No U2F devices configured')
|
||||
})
|
||||
|
||||
it('shows a HTTP warning', () => {
|
||||
const settings = shallowMount(PersonalSettings, {
|
||||
store,
|
||||
localVue,
|
||||
propsData: {
|
||||
httpWarning: true
|
||||
}
|
||||
})
|
||||
|
||||
expect(settings.text()).to.contain('You are accessing this site via an')
|
||||
})
|
||||
})
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2018 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/>.
|
||||
*/
|
||||
|
||||
require('jsdom-global')()
|
||||
|
||||
const t = (app, str) => str
|
||||
|
||||
require('vue').mixin({
|
||||
methods: {
|
||||
t
|
||||
}
|
||||
})
|
||||
|
||||
global.expect = require('chai').expect
|
||||
global.OC = {}
|
||||
global.t = t
|
|
@ -0,0 +1,51 @@
|
|||
const path = require('path');
|
||||
const { VueLoaderPlugin } = require('vue-loader');
|
||||
|
||||
module.exports = {
|
||||
entry: path.join(__dirname, 'main-settings.js'),
|
||||
output: {
|
||||
path: path.resolve(__dirname, '../js'),
|
||||
publicPath: '/js/',
|
||||
filename: 'settings.js'
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['vue-style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'sass-loader']
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|gif)$/,
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]?[hash]'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(svg)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'url-loader'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [new VueLoaderPlugin()],
|
||||
resolve: {
|
||||
extensions: ['*', '.js', '.vue']
|
||||
}
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
devtool: 'source-map',
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
const merge = require('webpack-merge')
|
||||
const common = require('./webpack.common.js')
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'production',
|
||||
devtool: 'source-map'
|
||||
})
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* @copyright 2018 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
* @author 2018 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/>.
|
||||
*/
|
||||
|
||||
const merge = require('webpack-merge');
|
||||
const common = require('./webpack.common.js');
|
||||
const nodeExternals = require('webpack-node-externals')
|
||||
|
||||
module.exports = merge(common, {
|
||||
mode: 'development',
|
||||
devtool: 'inline-cheap-module-source-map',
|
||||
externals: [nodeExternals()]
|
||||
})
|
|
@ -1,26 +1,8 @@
|
|||
<?php
|
||||
script('twofactor_u2f', 'build/settings');
|
||||
script('twofactor_u2f', 'settings');
|
||||
style('twofactor_u2f', 'style');
|
||||
?>
|
||||
|
||||
<div id="twofactor-u2f-settings">
|
||||
<span class="icon-loading-small u2f-loading"></span>
|
||||
<span><?php p($l->t('Loading your devices …')); ?></span>
|
||||
<input type="hidden" id="twofactor-u2f-initial-state" value="<?php p($_['state']); ?>">
|
||||
|
||||
<p class="utf-register-info" style="display: none;">
|
||||
<?php p($l->t('Please plug in your U2F device and press the device button to authorize.')) ?>
|
||||
</p>
|
||||
<p class="utf-register-info" style="display: none;">
|
||||
<em>
|
||||
<?php p($l->t('Chrome is the only browser that supports U2F devices. You need to install the "U2F Support Add-on" on Firefox to use U2F.')) ?>
|
||||
<p id="u2f-http-warning"
|
||||
style="display: none">
|
||||
<?php p($l->t('You are accessing this site via an insecure connection. Browsers might therefore refuse the U2F authentication.')) ?>
|
||||
</p>
|
||||
</em>
|
||||
</p>
|
||||
<p class="utf-register-success" style="display: none;">
|
||||
<span class="icon-checkmark-color"
|
||||
style="width: 16px;"></span><?php p($l->t('U2F device successfully registered.')) ?>
|
||||
</p>
|
||||
</div>
|
||||
<div id="twofactor-u2f-settings"></div>
|
||||
|
|
Загрузка…
Ссылка в новой задаче