Files
monica/resources/js/components/settings/WebauthnConnector.vue
T

403 lines
10 KiB
Vue

<style scoped>
.time {
color: gray;
}
</style>
<template>
<div class="form-group">
<notifications group="webauthn" position="top middle" :duration="5000" width="400" />
<div v-if="method === 'register-modal'">
<h3>{{ $t('settings.webauthn_title') }}</h3>
<div v-if="currentkeys !== null">
<ul class="table">
<li v-for="key in currentkeys"
:key="key.id"
class="table-row"
>
<div class="table-cell w-30">
<strong>{{ key.name }}</strong>
</div>
<div class="table-cell time w-50">
<template v-if="key.counter > 0">
{{ $t('settings.webauthn_last_use', {timestamp: formatTime(key.updated_at)}) }}
</template>
</div>
<div class="table-cell actions">
<a class="pointer" href="" @click.prevent="showDeleteModal(key.id)">
{{ $t('app.delete') }}
</a>
</div>
</li>
</ul>
</div>
<slot></slot>
<a v-if="isSupported" class="btn btn-primary" href="" @click.prevent="showRegisterModal">
{{ $t('settings.webauthn_enable_description') }}
</a>
<small v-else>
{{ notSupportedMessage() }}
</small>
<sweet-modal
id="registerModal"
ref="registerModal"
overlay-theme="dark"
:title="$t('settings.webauthn_title')"
>
<div v-if="registerTab === '1'">
<p>
{{ $t('settings.webauthn_key_name_help') }}
</p>
<form-input
:id="'keyName'"
v-model="keyName"
:title="$t('settings.webauthn_key_name')"
:value="keyName"
:input-type="'text'"
:width="150"
:required="true"
@keyup.enter="showRegisterModalTab('2');startRegister();"
/>
</div>
<div v-if="registerTab === '2'">
<div v-if="errorMessage !== ''" class="form-error-message mb3">
<div class="pa2">
<p class="mb0">
{{ errorMessage }}
</p>
<p>
<a href="" @click.prevent="startRegister()">
{{ $t('app.retry') }}
</a>
</p>
</div>
</div>
<div v-if="infoMessage !== ''" class="form-information-message mb3">
<div class="pa2">
<p class="mb0">
{{ infoMessage }}
</p>
</div>
</div>
<div v-if="errorMessage === ''" class="tc">
<img src="https://ssl.gstatic.com/accounts/strongauth/Challenge_2SV-Gnubby_graphic.png"
:alt="$t('settings.webauthn_insertKey')"
/>
</div>
<div v-if="errorMessage === ''" class="pa2">
<p>
{{ $t('settings.webauthn_insertKey') }}
</p>
<p>
{{ $t('settings.webauthn_buttonAdvise') }}
<br />
{{ $t('settings.webauthn_noButtonAdvise') }}
</p>
</div>
</div>
<div slot="button">
<a v-if="registerTab === '1'" class="btn" href="" @click.prevent="showRegisterModalTab('2');startRegister();">
{{ $t('pagination.next') }}
</a>
<a v-else class="btn" href="" @click.prevent="showRegisterModalTab('1')">
{{ $t('pagination.previous') }}
</a>
<a class="btn" href="" @click.prevent="closeRegisterModal()">
{{ $t('app.cancel') }}
</a>
</div>
</sweet-modal>
</div>
<div v-else>
<div v-if="errorMessage !== ''" class="form-error-message mb3">
<div class="pa2">
<p class="mb0">
{{ errorMessage }}
</p>
<p>
<a href="" @click.prevent="start()">
{{ $t('app.retry') }}
</a>
</p>
</div>
</div>
<div v-if="infoMessage !== ''" class="form-information-message mb3">
<div class="pa2">
<p class="mb0">
{{ infoMessage }}
</p>
</div>
</div>
<div class="tc">
<img src="https://ssl.gstatic.com/accounts/strongauth/Challenge_2SV-Gnubby_graphic.png"
:alt="$t('settings.webauthn_insertKey')"
/>
</div>
<div class="pa2">
<p>
{{ $t('settings.webauthn_insertKey') }}
</p>
<p>
{{ $t('settings.webauthn_buttonAdvise') }}
<br />
{{ $t('settings.webauthn_noButtonAdvise') }}
</p>
</div>
</div>
<sweet-modal ref="delete" overlay-theme="dark" title="Remove a key">
<form>
<div class="mb4">
{{ $t('settings.webauthn_delete_confirmation') }}
</div>
</form>
<div slot="button">
<a class="btn" href="" @click.prevent="closeDeleteModal()">
{{ $t('app.cancel') }}
</a>
<a class="btn" href="" @click.prevent="webauthnRemove(keyToTrash)">
{{ $t('app.delete') }}
</a>
</div>
</sweet-modal>
</div>
</template>
<script>
import { SweetModal } from 'sweet-modal-vue';
import * as WebAuthn from '../../../../vendor/asbiin/laravel-webauthn/resources/js/webauthn.js';
export default {
components: {
SweetModal
},
props: {
keys: {
type: Array,
default: function () {
return [];
}
},
publicKey: {
type: Object,
default: null,
},
method: {
type: String,
default: '',
},
timezone: {
type: String,
default: '',
},
script: {
type: String,
default: '',
},
},
data() {
return {
isSupported: true,
errorMessage: '',
infoMessage: '',
success: false,
currentkeys: [],
keyToTrash: '',
keyName: '',
registerTab: '',
data: null,
webauthn: null,
};
},
mounted() {
this.prepareComponent();
this.start();
},
methods: {
prepareComponent() {
this.currentkeys = this.keys;
this.data = this.registerdata;
this.webauthn = new WebAuthn((name, message) => {
this.errorMessage = this._errorMessage(name, message);
});
},
_errorMessage(name, message) {
switch (name) {
case 'InvalidStateError':
return this.$t('settings.webauthn_error_already_used');
case 'NotAllowedError':
return this.$t('settings.webauthn_error_not_allowed');
default:
return message;
}
},
notSupportedMessage() {
return this.$t('settings.webauthn_'+this.webauthn.notSupportedMessage());
},
start() {
var self = this;
this.errorMessage = '';
if (! this.webauthn.webAuthnSupport()) {
this.isSupported = false;
this.errorMessage = this.notSupportedMessage();
}
switch(this.method) {
case 'register':
setTimeout(function () {
self.webauthn.register(
self.publicKey,
function (datas) { self.webauthnRegisterCallback(datas, true); }
);
}, 10);
break;
case 'login':
this.webauthn.sign(
this.publicKey,
function (data) { self.webauthnLoginCallback(data); }
);
break;
}
},
showRegisterModal() {
this.errorMessage = '';
this.infoMessage = '';
this.keyName = '';
this.success = false;
this.showRegisterModalTab('1');
this.$refs.registerModal.open();
},
showRegisterModalTab(tab) {
this.registerTab = tab;
},
startRegister() {
var self = this;
this.errorMessage = '';
axios.post('webauthn/keys/options')
.then(response => {
if (self.registerTab === '2') {
var data = response.data.publicKey;
setTimeout(function () {
self.webauthn.register(
data,
function (datas) { self.webauthnRegisterCallback(datas, false); }
);
}, 10);
}
}).catch(error => {
self.notify(error.response.data.message, false);
});
},
closeRegisterModal() {
this.$refs.registerModal.close();
this.showRegisterModalTab('');
},
webauthnRegisterCallback(data, redirect) {
var self = this;
axios.post('webauthn/keys', {
...data,
name: self.keyName,
}).then(response => {
self.success = true;
self.notify(self.$t('settings.webauthn_success'), true);
self.currentkeys.push({
id: response.result.id,
name: response.result.name,
});
}).then(response => {
if (redirect) {
setTimeout(function () {
window.location = response.data.callback;
}, 100);
} else {
self.closeRegisterModal();
}
}).catch(error => {
self.errorMessage = error.message ? error.message : error.response.data.message;
});
},
webauthnLoginCallback(data) {
var self = this;
axios.post('webauthn/auth', {
...data
}).then(response => {
self.success = true;
self.notify(self.$t('settings.webauthn_success'), true);
window.location = response.data.callback;
}).catch(error => {
self.errorMessage = error.message ? error.message : error.response.data.message;
});
},
webauthnRemove(id) {
var self = this;
axios.delete('webauthn/keys/'+id)
.then(response => {
self.currentkeys.splice(self.currentkeys.indexOf(self.currentkeys.find(item => item.id === response.data.id)), 1);
self.success = true;
self.notify(self.$t('settings.webauthn_delete_success'), true);
self.closeDeleteModal();
}).catch(error => {
self.errorMessage = error.response.data.message;
});
},
showDeleteModal(id) {
this.keyToTrash = id;
this.$refs.delete.open();
},
closeDeleteModal() {
this.$refs.delete.close();
},
formatTime(value) {
var moment = require('moment-timezone');
moment.locale(this._i18n.locale);
moment.tz.setDefault('UTC');
var t = moment(value);
var date = moment.tz(t, this.timezone);
return date.format('LLLL');
},
notify(text, success) {
this.$notify({
group: 'webauthn',
title: text,
text: '',
type: success ? 'success' : 'error'
});
}
}
};
</script>