Move live scanner to a dedicated view to make things easier

This commit is contained in:
Bubka 2020-11-23 19:30:58 +01:00
parent 0989e6e452
commit 0d84a75778
8 changed files with 200 additions and 211 deletions

1
resources/js/app.js vendored
View File

@ -5,7 +5,6 @@ import api from './api'
import i18n from './langs/i18n'
import FontAwesome from './packages/fontawesome'
import Clipboard from './packages/clipboard'
import QrcodeReader from './packages/qrcodeReader'
import Notifications from 'vue-notification'
import './components'

View File

@ -1,4 +0,0 @@
import Vue from 'vue'
import QrcodeReader from 'vue-qrcode-reader'
Vue.use(QrcodeReader)

View File

@ -4,6 +4,7 @@ import Router from 'vue-router'
Vue.use(Router)
import Start from './views/Start'
import Capture from './views/Capture'
import Accounts from './views/Accounts'
import CreateAccount from './views/twofaccounts/Create'
import EditAccount from './views/twofaccounts/Edit'
@ -22,6 +23,7 @@ const router = new Router({
mode: 'history',
routes: [
{ path: '/start', name: 'start', component: Start, meta: { requiresAuth: true }, props: true },
{ path: '/capture', name: 'capture', component: Capture, meta: { requiresAuth: true }, props: true },
{ path: '/accounts', name: 'accounts', component: Accounts, meta: { requiresAuth: true }, alias: '/', props: true },
{ path: '/account/create', name: 'createAccount', component: CreateAccount, meta: { requiresAuth: true } },

View File

@ -304,6 +304,9 @@
if( this.$root.appSettings.useDirectCapture && this.$root.appSettings.defaultCaptureMode === 'advancedForm' ) {
this.$router.push({ name: 'createAccount' })
}
else if( this.$root.appSettings.useDirectCapture && this.$root.appSettings.defaultCaptureMode === 'livescan' ) {
this.$router.push({ name: 'capture' })
}
else {
this.$router.push({ name: 'start' })
}

View File

@ -0,0 +1,118 @@
<template>
<div class="modal is-active">
<div class="modal-background"></div>
<div class="modal-content">
<section class="section">
<div class="columns is-centered">
<div class="column is-three-quarters">
<div class="box has-text-centered has-background-black-ter is-shadowless">
<div v-if="errorText">
<p class="block is-size-5">{{ $t('twofaccounts.stream.live_scan_cant_start') }}</p>
<p class="has-text-light block">{{ $t('twofaccounts.stream.' + errorText + '.reason') }}</p>
<p class="is-size-7">{{ $t('twofaccounts.stream.' + errorText + '.solution') }}</p>
</div>
<span v-else class="is-size-4 has-text-light">
<font-awesome-icon :icon="['fas', 'spinner']" size="2x" spin />
</span>
</div>
</div>
</div>
</section>
</div>
<div class="fullscreen-streamer">
<qrcode-stream @decode="submitUri" @init="onStreamerInit" camera="auto" />
</div>
<div class="fullscreen-footer">
<!-- Cancel button -->
<label class="button is-large is-warning is-rounded" @click="exitStream()">
{{ $t('commons.cancel') }}
</label>
</div>
</div>
</template>
<script>
import { QrcodeStream } from 'vue-qrcode-reader'
import Form from './../components/Form'
export default {
data(){
return {
showStream: true,
errorText: '',
form: new Form({
qrcode: null,
uri: '',
}),
}
},
components: {
QrcodeStream,
},
methods: {
exitStream() {
this.camera = 'off'
this.$router.go(-1)
},
async onStreamerInit (promise) {
try {
await promise
}
catch (error) {
if (error.name === 'NotAllowedError') {
this.errorText = 'need_grant_permission'
} else if (error.name === 'NotReadableError') {
this.errorText = 'not_readable'
} else if (error.name === 'NotFoundError') {
this.errorText = 'no_cam_on_device'
} else if (error.name === 'NotSupportedError' || error.name === 'InsecureContextError') {
this.errorText = 'secured_context_required'
} else if (error.name === 'OverconstrainedError') {
this.errorText = 'camera_not_suitable'
} else if (error.name === 'StreamApiNotSupportedError') {
this.errorText = 'stream_api_not_supported'
}
}
},
/**
* The basicQRcodeReader option is Off, so qrcode decoding has already be done by vue-qrcode-reader, whether
* from livescan or file input.
* We simply check the uri validity to prevent useless push to the Create form, but the form will check uri validity too.
*/
async submitUri(event) {
this.form.uri = event
if( !this.form.uri ) {
this.$notify({type: 'is-warning', text: this.$t('errors.qrcode_cannot_be_read') })
}
else if( this.form.uri.slice(0, 15 ).toLowerCase() !== "otpauth://totp/" && this.form.uri.slice(0, 15 ).toLowerCase() !== "otpauth://hotp/" ) {
this.$notify({type: 'is-warning', text: this.$t('errors.no_valid_otp') })
}
else {
this.pushUriToCreateForm(this.form.uri)
}
},
pushUriToCreateForm(data) {
this.$router.push({ name: 'createAccount', params: { decodedUri: data } });
}
}
}
</script>

View File

@ -1,76 +1,53 @@
<template>
<div id="quick-uploader">
<!-- static landing UI -->
<div v-show="!(showStream && canStream)" class="container has-text-centered">
<div class="columns quick-uploader">
<!-- trailer phrase that invite to add an account -->
<div class="column is-full quick-uploader-header" :class="{ 'is-invisible' : accountCount > 0 }">
{{ $t('twofaccounts.no_account_here') }}<br>
{{ $t('twofaccounts.add_first_account') }}
</div>
<!-- Livescan button -->
<div class="column is-full quick-uploader-button" >
<div class="quick-uploader-centerer">
<!-- upload a qr code (with basic file field and backend decoding) -->
<label v-if="$root.appSettings.useBasicQrcodeReader" class="button is-link is-medium is-rounded is-focused" ref="qrcodeInputLabel">
<input class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
{{ $t('twofaccounts.forms.upload_qrcode') }}
</label>
<!-- scan button that launch camera stream -->
<label v-else class="button is-link is-medium is-rounded is-focused" @click="enableStream()">
{{ $t('twofaccounts.forms.scan_qrcode') }}
</label>
</div>
</div>
<!-- alternative methods -->
<div class="column is-full">
<div class="block has-text-light">{{ $t('twofaccounts.forms.alternative_methods') }}</div>
<!-- upload a qr code (with qrcode-capture component) -->
<div class="block has-text-link" v-if="!$root.appSettings.useBasicQrcodeReader">
<form @submit.prevent="createAccount" @keydown="form.onKeydown($event)">
<label :class="{'is-loading' : form.isBusy}" class="button is-link is-outlined is-rounded" ref="qrcodeInputLabel">
<qrcode-capture @decode="submitUri" class="file-input" ref="qrcodeInput" />
{{ $t('twofaccounts.forms.upload_qrcode') }}
</label>
<field-error :form="form" field="qrcode" />
<field-error :form="form" field="uri" />
</form>
</div>
<!-- link to advanced form -->
<div class="block has-text-link">
<router-link class="button is-link is-outlined is-rounded" :to="{ name: 'createAccount' }" >
{{ $t('twofaccounts.forms.use_advanced_form') }}
</router-link>
</div>
<!-- static landing UI -->
<div class="container has-text-centered">
<div class="columns quick-uploader">
<!-- trailer phrase that invite to add an account -->
<div class="column is-full quick-uploader-header" :class="{ 'is-invisible' : accountCount > 0 }">
{{ $t('twofaccounts.no_account_here') }}<br>
{{ $t('twofaccounts.add_first_account') }}
</div>
<!-- Livescan button -->
<div class="column is-full quick-uploader-button" >
<div class="quick-uploader-centerer">
<!-- upload a qr code (with basic file field and backend decoding) -->
<label v-if="$root.appSettings.useBasicQrcodeReader" class="button is-link is-medium is-rounded is-focused" ref="qrcodeInputLabel">
<input class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
{{ $t('twofaccounts.forms.upload_qrcode') }}
</label>
<!-- scan button that launch camera stream -->
<label v-else class="button is-link is-medium is-rounded is-focused" @click="capture()">
{{ $t('twofaccounts.forms.scan_qrcode') }}
</label>
</div>
</div>
<!-- Footer -->
<vue-footer :showButtons="true" v-if="accountCount > 0">
<!-- back button -->
<p class="control">
<router-link class="button is-dark is-rounded" :to="{ name: 'accounts' }" >
{{ $t('commons.back') }}
<!-- alternative methods -->
<div class="column is-full">
<div class="block has-text-light">{{ $t('twofaccounts.forms.alternative_methods') }}</div>
<!-- upload a qr code -->
<div class="block has-text-link" v-if="!$root.appSettings.useBasicQrcodeReader">
<label class="button is-link is-outlined is-rounded" ref="qrcodeInputLabel">
<input class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
{{ $t('twofaccounts.forms.upload_qrcode') }}
</label>
</div>
<!-- link to advanced form -->
<div class="block has-text-link">
<router-link class="button is-link is-outlined is-rounded" :to="{ name: 'createAccount' }" >
{{ $t('twofaccounts.forms.use_advanced_form') }}
</router-link>
</p>
</vue-footer>
</div>
<!-- camera stream fullscreen scanner -->
<div v-show="showStream && canStream">
<div class="fullscreen-alert has-text-centered">
<span class="is-size-4 has-text-light">
<font-awesome-icon :icon="['fas', 'spinner']" size="2x" spin />
</span>
</div>
<div class="fullscreen-streamer">
<qrcode-stream @decode="submitUri" @init="onStreamerInit" :camera="camera" />
</div>
<div class="fullscreen-footer">
<!-- Cancel button -->
<label class="button is-large is-warning is-rounded" @click="disableStream()">
{{ $t('commons.cancel') }}
</label>
</div>
</div>
</div>
<!-- Footer -->
<vue-footer :showButtons="true" v-if="accountCount > 0">
<!-- back button -->
<p class="control">
<router-link class="button is-dark is-rounded" :to="{ name: 'accounts' }" >
{{ $t('commons.back') }}
</router-link>
</p>
</vue-footer>
</div>
</template>
@ -82,27 +59,12 @@
* route: '/start'
*
* Offer the user all available possibilities for capturing an account :
* - By decoding a QR code acquired by live scan
* - By sending the user to the live scanner
* - By decoding a QR code submitted with a form 'File' field
* - By browsing to the advanced form
*
* 2 QR code decoders are implemented :
* - vue-qrcode-reader (the default one)
* ~ QR codes are acquired by live scan or with a 'File' field by the js front-end
* ~ Decoding is done by the js front-end, there is no call to the back-end API
* - chillerlan/php-qrcode (aka 'BasicQrcodeReader')
* ~ QR codes are acquired with a 'File' field and uploaded to the php backend
* ~ Decoding is done by the php back-end
*
* Output : both decoders provide an URI and push it the Create form
*
* The view behavior is affected by the user options :
* - 'appSettings.useBasicQrcodeReader' totally disable the vue-qrcode-reader decoder
* - 'appSettings.useDirectCapture' trigger the acquisition mode set by 'appSettings.defaultCaptureMode' automatically at vue @created event
* - By sending the user to the advanced form
*
*/
import Form from './../components/Form'
export default {
name: 'Start',
@ -110,20 +72,9 @@
data(){
return {
accountCount: null,
form: new Form({
qrcode: null,
uri: '',
}),
errorName: '',
errorText: '',
showStream: false,
canStream: null,
camera: 'auto',
}
},
// props: ['accountCount'],
mounted() {
this.axios.get('api/twofaccounts/count').then(response => {
@ -138,94 +89,12 @@
this.$refs.qrcodeInputLabel.click()
}
})
if( this.$root.appSettings.useBasicQrcodeReader ) {
// User has set the basic QR code reader (run by backend) so we disable the other one (run by js)
this.canStream = this.showStream = false
}
else if( this.accountCount > 0 && this.$root.appSettings.useDirectCapture ) {
if( this.$root.appSettings.defaultCaptureMode === 'livescan' ) {
this.enableStream()
}
}
},
beforeDestroy() {
this.form.clear()
},
methods: {
async enableStream() {
this.camera = 'auto'
if( this.canStream ) {
this.showStream = true
console.log('stream started')
}
else if( this.errorText && !this.$root.appSettings.useBasicQrcodeReader ) {
this.$notify({ type: 'is-warning', text: this.errorText })
}
},
async disableStream() {
this.camera = 'off'
this.showStream = false
console.log('stream stopped')
},
async onStreamerInit (promise) {
this.errorText = ''
this.errorName = ''
try {
await promise
this.canStream = true
console.log('stream is possible')
}
catch (error) {
this.errorName = error.name
if (error.name === 'NotAllowedError') {
this.errorText = this.$t('twofaccounts.stream.need_grant_permission')
} else if (error.name === 'NotReadableError') {
this.errorText = this.$t('twofaccounts.stream.not_readable')
} else if (error.name === 'NotFoundError') {
this.errorText = this.$t('twofaccounts.stream.no_cam_on_device')
} else if (error.name === 'NotSupportedError' || error.name === 'InsecureContextError') {
this.errorText = this.$t('twofaccounts.stream.secured_context_required')
} else if (error.name === 'OverconstrainedError') {
this.errorText = this.$t('twofaccounts.stream.camera_not_suitable')
} else if (error.name === 'StreamApiNotSupportedError') {
this.errorText = this.$t('twofaccounts.stream.stream_api_not_supported')
}
this.canStream = false
if( !this.$root.appSettings.useBasicQrcodeReader && this.$root.appSettings.useDirectCapture && this.$root.appSettings.defaultCaptureMode === 'livescan') {
this.$notify({ type: 'is-warning', text: this.errorText })
}
console.log('stream no possible : ' + this.errorText)
}
},
/**
* the basicQRcodeReader option is On, so qrcode decoding will be done by the backend, which in return
* send the corresponding URI
* Send the submitted QR code to the backend for decoding then push ser to the create form
*/
async submitQrCode() {
@ -235,34 +104,16 @@
const { data } = await this.form.upload('/api/qrcode/decode', imgdata)
this.pushUriToCreateForm(data.uri)
this.$router.push({ name: 'createAccount', params: { decodedUri: data.uri } });
},
/**
* The basicQRcodeReader option is Off, so qrcode decoding has already be done by vue-qrcode-reader, whether
* from livescan or file input.
* We simply check the uri validity to prevent useless push to the Create form, but the form will check uri validity too.
* Push user to the dedicated capture view for live scan
*/
async submitUri(event) {
this.form.uri = event
if( !this.form.uri ) {
this.$notify({type: 'is-warning', text: this.$t('errors.qrcode_cannot_be_read') })
}
else if( this.form.uri.slice(0, 15 ).toLowerCase() !== "otpauth://totp/" && this.form.uri.slice(0, 15 ).toLowerCase() !== "otpauth://hotp/" ) {
this.$notify({type: 'is-warning', text: this.$t('errors.no_valid_otp') })
}
else {
this.pushUriToCreateForm(this.form.uri)
}
capture() {
this.$router.push({ name: 'capture' });
},
pushUriToCreateForm(data) {
this.$router.push({ name: 'createAccount', params: { decodedUri: data } });
}
}
};

View File

@ -87,13 +87,32 @@
'alternative_methods' => 'Alternative methods',
],
'stream' => [
'need_grant_permission' => 'You need to grant camera access permission',
'not_readable' => 'Fail to load scanner. Is the camera already in use?',
'no_cam_on_device' => 'No camera on this device',
'secured_context_required' => 'Secure context required (HTTPS or localhost)',
'live_scan_cant_start' => 'Live scan can\'t start :(',
'need_grant_permission' => [
'reason' => '2FAuth does not have permission to access your camera',
'solution' => 'You need to grant permission to use your device camera. If you already denied and your browser do not prompt you again, please refers to the browser documentation to find out how to grant permission.'
],
'not_readable' => [
'reason' => 'Fail to load scanner',
'solution' => 'Is the camera already in use? Ensure that no other app use your camera and try again'
],
'no_cam_on_device' => [
'reason' => 'No camera on this device',
'solution' => 'Maybe your forget to plug in your webcam'
],
'secured_context_required' => [
'reason' => 'Secure context required',
'solution' => 'HTTPS is required for live scan. If you run 2FAuth from your computer, do not use virtual host other than localhost'
],
'https_required' => 'HTTPS required for camera streaming',
'camera_not_suitable' => 'Installed cameras are not suitable',
'stream_api_not_supported' => 'Stream API is not supported in this browser'
'camera_not_suitable' => [
'reason' => 'Installed cameras are not suitable',
'solution' => 'Please use another device/camera'
],
'stream_api_not_supported' => [
'reason' => 'Stream API is not supported in this browser',
'solution' => 'You should use a modern browser'
],
],
'confirm' => [
'delete' => 'Are you sure you want to delete this account?',

View File

@ -227,6 +227,7 @@ a:hover {
left: 0;
width: 100%;
height: 65%;
padding: 2%;
}
/* Corner borders */