mirror of
https://github.com/Bubka/2FAuth.git
synced 2024-12-23 23:49:53 +01:00
Merge branch 'feature/vueQrcodeReader' into dev
This commit is contained in:
commit
1acedf5e28
@ -6,11 +6,11 @@ class Options
|
||||
{
|
||||
|
||||
/**
|
||||
* Build a collection of options to apply
|
||||
* Compile both default and user options
|
||||
*
|
||||
* @return Options collection
|
||||
* @return Options collection or a signle
|
||||
*/
|
||||
public static function get()
|
||||
public static function get($option = null)
|
||||
{
|
||||
// Get a collection of user saved options
|
||||
$userOptions = \Illuminate\Support\Facades\DB::table('options')->pluck('value', 'key');
|
||||
@ -32,7 +32,7 @@ public static function get()
|
||||
// fallback values for every options
|
||||
$options = collect(config('app.options'))->merge($userOptions);
|
||||
|
||||
return $options;
|
||||
return !is_null($option) ? $options[$option] : $options;
|
||||
}
|
||||
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
use Zxing\QrReader;
|
||||
use OTPHP\TOTP;
|
||||
use OTPHP\Factory;
|
||||
use App\Classes\Options;
|
||||
use Assert\AssertionFailedException;
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Http\Request;
|
||||
@ -21,19 +22,30 @@ class QrCodecontroller extends Controller
|
||||
public function decode(Request $request)
|
||||
{
|
||||
|
||||
// input validation
|
||||
$this->validate($request, [
|
||||
'qrcode' => 'required|image',
|
||||
]);
|
||||
if(Options::get('useBasicQrcodeReader')) {
|
||||
|
||||
// qrcode analysis
|
||||
$path = $request->file('qrcode')->store('qrcodes');
|
||||
$qrcode = new QrReader(storage_path('app/' . $path));
|
||||
// input validation
|
||||
$this->validate($request, [
|
||||
'qrcode' => 'required|image',
|
||||
]);
|
||||
|
||||
$uri = urldecode($qrcode->text());
|
||||
// qrcode analysis
|
||||
$path = $request->file('qrcode')->store('qrcodes');
|
||||
$qrcode = new QrReader(storage_path('app/' . $path));
|
||||
|
||||
// delete uploaded file
|
||||
Storage::delete($path);
|
||||
$uri = urldecode($qrcode->text());
|
||||
|
||||
// delete uploaded file
|
||||
Storage::delete($path);
|
||||
}
|
||||
else {
|
||||
|
||||
$this->validate($request, [
|
||||
'uri' => 'required|string',
|
||||
]);
|
||||
|
||||
$uri = $request->uri;
|
||||
}
|
||||
|
||||
// return the OTP object
|
||||
try {
|
||||
|
@ -35,6 +35,7 @@
|
||||
'isDemoApp' => env('IS_DEMO_APP', false),
|
||||
'showTokenAsDot' => false,
|
||||
'closeTokenOnCopy' => false,
|
||||
'useBasicQrcodeReader' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|
64
package-lock.json
generated
64
package-lock.json
generated
@ -1570,6 +1570,22 @@
|
||||
"object.assign": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"babel-runtime": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||
"requires": {
|
||||
"core-js": "^2.4.0",
|
||||
"regenerator-runtime": "^0.11.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"regenerator-runtime": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
@ -1991,6 +2007,11 @@
|
||||
"caller-callsite": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"callforth": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/callforth/-/callforth-0.3.1.tgz",
|
||||
"integrity": "sha512-Q2zPfqnwoKsb1DTVCr4lmhe49wKNBsMmNlbudjleu3/co+Nw1pOqFHYJHrW3VZ253ou9AAr+xauQR0C55NPdzA=="
|
||||
},
|
||||
"callsites": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz",
|
||||
@ -2501,6 +2522,11 @@
|
||||
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=",
|
||||
"dev": true
|
||||
},
|
||||
"core-js": {
|
||||
"version": "2.6.11",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
|
||||
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.6.2.tgz",
|
||||
@ -5510,6 +5536,11 @@
|
||||
"graceful-fs": "^4.1.6"
|
||||
}
|
||||
},
|
||||
"jsqr": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.2.0.tgz",
|
||||
"integrity": "sha512-wKcQS9QC2VHGk7aphWCp1RrFyC0CM6fMgC5prZZ2KV/Lk6OKNoCod9IR6bao+yx3KPY0gZFC5dc+h+KFzCI0Wg=="
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
@ -7908,6 +7939,14 @@
|
||||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"rtcpeerconnection-shim": {
|
||||
"version": "1.2.15",
|
||||
"resolved": "https://registry.npmjs.org/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.15.tgz",
|
||||
"integrity": "sha512-C6DxhXt7bssQ1nHb154lqeL0SXz5Dx4RczXZu2Aa/L1NJFnEVDxFwCBo3fqtuljhHIGceg5JKBV4XJ0gW5JKyw==",
|
||||
"requires": {
|
||||
"sdp": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"run-queue": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz",
|
||||
@ -7984,6 +8023,11 @@
|
||||
"ajv-keywords": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"sdp": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/sdp/-/sdp-2.12.0.tgz",
|
||||
"integrity": "sha512-jhXqQAQVM+8Xj5EjJGVweuEzgtGWb3tmEEpl3CLP3cStInSbVHSg0QWOGQzNq8pSID4JkpeV2mPqlMDLrm0/Vw=="
|
||||
},
|
||||
"select-hose": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
|
||||
@ -9289,6 +9333,17 @@
|
||||
"vue": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"vue-qrcode-reader": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vue-qrcode-reader/-/vue-qrcode-reader-2.1.1.tgz",
|
||||
"integrity": "sha512-rIxV0RAuiomNi4n03L7XVbCKRtq4sT1LYtIk3osuBdJA/1W6y8yDtP4SvGpBdRCLaurfHaicpAZxQB98mejSCg==",
|
||||
"requires": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"callforth": "^0.3.0",
|
||||
"jsqr": "^1.2.0",
|
||||
"webrtc-adapter": "^6.2.1"
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.6.tgz",
|
||||
@ -9664,6 +9719,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"webrtc-adapter": {
|
||||
"version": "6.4.8",
|
||||
"resolved": "https://registry.npmjs.org/webrtc-adapter/-/webrtc-adapter-6.4.8.tgz",
|
||||
"integrity": "sha512-YM8yl545c/JhYcjGHgaCoA7jRK/KZuMwEDFeP2AcP0Auv5awEd+gZE0hXy9z7Ed3p9HvAXp8jdbe+4ESb1zxAw==",
|
||||
"requires": {
|
||||
"rtcpeerconnection-shim": "^1.2.14",
|
||||
"sdp": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"websocket-driver": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz",
|
||||
|
@ -34,6 +34,7 @@
|
||||
"vue-axios": "^2.1.5",
|
||||
"vue-i18n": "^8.16.0",
|
||||
"vue-pull-refresh": "^0.2.7",
|
||||
"vue-qrcode-reader": "^2.1.1",
|
||||
"vue-router": "^3.1.6",
|
||||
"vuedraggable": "^2.23.2"
|
||||
}
|
||||
|
1
resources/js/app.js
vendored
1
resources/js/app.js
vendored
@ -4,6 +4,7 @@ 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 App from './components/App'
|
||||
|
||||
import './components'
|
||||
|
218
resources/js/components/QuickUploader.vue
Normal file
218
resources/js/components/QuickUploader.vue
Normal file
@ -0,0 +1,218 @@
|
||||
<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' : !showTrailer }">
|
||||
{{ $t('twofaccounts.no_account_here') }}<br>
|
||||
{{ $t('twofaccounts.add_first_account') }}
|
||||
</div>
|
||||
<!-- add button -->
|
||||
<div class="column is-full quick-uploader-button" >
|
||||
<div class="quick-uploader-centerer">
|
||||
<!-- scan button that launch camera stream -->
|
||||
<label v-if="canStream" class="button is-link is-medium is-rounded is-focused" @click="enableStream()">
|
||||
{{ $t('twofaccounts.forms.scan_qrcode') }}
|
||||
</label>
|
||||
<!-- or classic input field -->
|
||||
<form v-else @submit.prevent="createAccount" @keydown="form.onKeydown($event)">
|
||||
<label :class="{'is-loading' : form.isBusy}" class="button is-link is-medium is-rounded is-focused">
|
||||
<input v-if="$root.appSettings.useBasicQrcodeReader" class="file-input" type="file" accept="image/*" v-on:change="uploadQrcode" ref="qrcodeInput">
|
||||
<qrcode-capture v-else @decode="uploadQrcode" class="file-input" ref="qrcodeInput" />
|
||||
{{ $t('twofaccounts.forms.use_qrcode.val') }}
|
||||
</label>
|
||||
<field-error :form="form" field="qrcode" />
|
||||
<field-error :form="form" field="uri" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Fallback link to classic form -->
|
||||
<div class="column is-full quick-uploader-footer">
|
||||
<router-link :to="{ name: 'create' }" class="is-link">{{ $t('twofaccounts.use_full_form') }}</router-link>
|
||||
</div>
|
||||
<div v-if="showError" class="column is-full quick-uploader-footer">
|
||||
<notification :message="errorText" :isDeletable="false" type="is-danger" />
|
||||
</div>
|
||||
</div>
|
||||
</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="uploadQrcode" @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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Form from './Form'
|
||||
|
||||
export default {
|
||||
name: 'QuickUploader',
|
||||
|
||||
data(){
|
||||
return {
|
||||
form: new Form({
|
||||
qrcode: null,
|
||||
uri: '',
|
||||
}),
|
||||
errorName: '',
|
||||
errorText: '',
|
||||
showStream: false,
|
||||
canStream: true,
|
||||
camera: 'auto',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
|
||||
debugMode: function() {
|
||||
return process.env.NODE_ENV
|
||||
},
|
||||
|
||||
showError: function() {
|
||||
return this.debugMode == 'development' && this.errorName == 'NotAllowedError'
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
showTrailer: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
|
||||
directStreaming: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
if( this.$root.appSettings.useBasicQrcodeReader ) {
|
||||
// User has set the basic QR code reader so we disable the modern one
|
||||
this.canStream = this.showStream = false
|
||||
}
|
||||
else {
|
||||
if( this.directStreaming ) {
|
||||
this.enableStream()
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
this.form.clear()
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
async enableStream() {
|
||||
|
||||
this.$parent.$emit('initStreaming')
|
||||
|
||||
this.camera = 'auto'
|
||||
this.showStream = true
|
||||
|
||||
console.log('stream enabled')
|
||||
},
|
||||
|
||||
async disableStream() {
|
||||
|
||||
this.camera = 'off'
|
||||
this.showStream = false
|
||||
|
||||
this.$parent.$emit('stopStreaming')
|
||||
},
|
||||
|
||||
async onStreamerInit (promise) {
|
||||
|
||||
this.errorText = ''
|
||||
this.errorName = ''
|
||||
|
||||
try {
|
||||
await promise
|
||||
}
|
||||
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.setUploader()
|
||||
},
|
||||
|
||||
setUploader() {
|
||||
|
||||
if( this.errorName ) {
|
||||
this.canStream = false
|
||||
console.log(this.errorText)
|
||||
}
|
||||
|
||||
if( !this.errorName && !this.showStream ) {
|
||||
this.camera = 'off'
|
||||
}
|
||||
|
||||
if( this.canStream && this.showStream) {
|
||||
this.$parent.$emit('startStreaming')
|
||||
}
|
||||
},
|
||||
|
||||
async uploadQrcode(event) {
|
||||
|
||||
var response
|
||||
|
||||
if(this.$root.appSettings.useBasicQrcodeReader) {
|
||||
let imgdata = new FormData();
|
||||
imgdata.append('qrcode', this.$refs.qrcodeInput.files[0]);
|
||||
|
||||
response = await this.form.upload('/api/qrcode/decode', imgdata)
|
||||
}
|
||||
else {
|
||||
// We post the decoded URI instead of an image to decode
|
||||
this.form.uri = event
|
||||
|
||||
if( !this.form.uri ) {
|
||||
return false
|
||||
}
|
||||
|
||||
response = await this.form.post('/api/qrcode/decode')
|
||||
}
|
||||
|
||||
this.$router.push({ name: 'create', params: { qrAccount: response.data } });
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
30
resources/js/langs/locales.js
vendored
30
resources/js/langs/locales.js
vendored
@ -97,6 +97,10 @@ export default {
|
||||
"close_token_on_copy": {
|
||||
"label": "Close token after copy",
|
||||
"help": "Automatically close the popup showing the generated token after it has been copied"
|
||||
},
|
||||
"use_basic_qrcode_reader": {
|
||||
"label": "Use basic qrcode reader",
|
||||
"help": "If you experiences issues when capturing qrCodes enables this option to switch to a more basic but more reliable qrcode reader"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -107,7 +111,7 @@ export default {
|
||||
"new": "New",
|
||||
"no_account_here": "No 2FA here!",
|
||||
"add_first_account": "Add your first account",
|
||||
"use_full_form": "Use the full form",
|
||||
"use_full_form": "Or use the full form",
|
||||
"add_one": "Add one",
|
||||
"manage": "Manage",
|
||||
"done": "Done",
|
||||
@ -122,6 +126,7 @@ export default {
|
||||
"edit_account": "Edit account",
|
||||
"otp_uri": "OTP Uri",
|
||||
"hotp_counter": "HOTP Counter",
|
||||
"scan_qrcode": "Scan a qrcode",
|
||||
"use_qrcode": {
|
||||
"val": "Use a qrcode",
|
||||
"title": "Use a QR code to fill the form magically"
|
||||
@ -139,6 +144,14 @@ export default {
|
||||
"save": "Save",
|
||||
"test": "Test"
|
||||
},
|
||||
"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)",
|
||||
"camera_not_suitable": "Installed cameras are not suitable",
|
||||
"stream_api_not_supported": "Stream API is not supported in this browser"
|
||||
},
|
||||
"confirm": {
|
||||
"delete": "Are you sure you want to delete this account?",
|
||||
"cancel": "The account will be lost. Are you sure?"
|
||||
@ -366,6 +379,10 @@ export default {
|
||||
"close_token_on_copy": {
|
||||
"label": "Ne plus afficher les codes copiés",
|
||||
"help": "Ferme automatiquement le popup affichant le code généré dès que ce dernier a été copié."
|
||||
},
|
||||
"use_basic_qrcode_reader": {
|
||||
"label": "Utiliser le lecteur de qrcode basique",
|
||||
"help": "Si vous rencontrez des problèmes lors de la lecture des qrCodes activez cette option pour utiliser un lecteur de qrcode moins évolué mais plus largement compatible"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -376,7 +393,7 @@ export default {
|
||||
"new": "Nouveau",
|
||||
"no_account_here": "Aucun compte 2FA !",
|
||||
"add_first_account": "Ajouter votre premier compte",
|
||||
"use_full_form": "Utiliser le formulaire détaillé",
|
||||
"use_full_form": "Ou utiliser le formulaire détaillé",
|
||||
"add_one": "Add one",
|
||||
"manage": "Gérer",
|
||||
"done": "Terminé",
|
||||
@ -391,6 +408,7 @@ export default {
|
||||
"edit_account": "Modifier le compte",
|
||||
"otp_uri": "OTP Uri",
|
||||
"hotp_counter": "Compteur HOTP",
|
||||
"scan_qrcode": "Scanner un QR code",
|
||||
"use_qrcode": {
|
||||
"val": "Utiliser un QR code",
|
||||
"title": "Utiliser un QR code pour renseigner le formulaire d'un seul coup d'un seul"
|
||||
@ -408,6 +426,14 @@ export default {
|
||||
"save": "Enregistrer",
|
||||
"test": "Tester"
|
||||
},
|
||||
"stream": {
|
||||
"need_grant_permission": "Vous devez autoriser l'utilisation de votre caméra",
|
||||
"not_readable": "Le scanner ne se charge pas. La caméra est-elle déjà utilisée ?",
|
||||
"no_cam_on_device": "Votre équipement ne dispose pas de caméra",
|
||||
"secured_context_required": "Contexte sécurisé requis (HTTPS ou localhost)",
|
||||
"camera_not_suitable": "Votre équipement ne dispose pas d'une caméra adaptée",
|
||||
"stream_api_not_supported": "L'API Stream n'est pas supportée par votre navigateur"
|
||||
},
|
||||
"confirm": {
|
||||
"delete": "Etes-vous sûrs de vouloir supprimer le compte ?",
|
||||
"cancel": "Les données seront perdues, êtes-vous sûrs ?"
|
||||
|
6
resources/js/packages/fontawesome.js
vendored
6
resources/js/packages/fontawesome.js
vendored
@ -15,7 +15,8 @@ import {
|
||||
faLockOpen,
|
||||
faSearch,
|
||||
faEllipsisH,
|
||||
faBars
|
||||
faBars,
|
||||
faSpinner
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
library.add(
|
||||
@ -29,7 +30,8 @@ library.add(
|
||||
faLockOpen,
|
||||
faSearch,
|
||||
faEllipsisH,
|
||||
faBars
|
||||
faBars,
|
||||
faSpinner
|
||||
);
|
||||
|
||||
Vue.component('font-awesome-icon', FontAwesomeIcon)
|
4
resources/js/packages/qrcodeReader.js
vendored
Normal file
4
resources/js/packages/qrcodeReader.js
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
import Vue from 'vue'
|
||||
import QrcodeReader from 'vue-qrcode-reader'
|
||||
|
||||
Vue.use(QrcodeReader)
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- show accounts list -->
|
||||
<div class="container" v-if="this.showAccounts">
|
||||
<!-- header -->
|
||||
<div class="columns is-gapless is-mobile is-centered">
|
||||
@ -32,8 +33,8 @@
|
||||
readyLabel: '',
|
||||
loadingLabel: 'refreshing'
|
||||
}" > -->
|
||||
<draggable v-model="filteredAccounts" @start="drag = true" @end="saveOrder" ghost-class="ghost" handle=".tfa-dots" animation="200" class="accounts columns is-multiline is-centered">
|
||||
<transition-group type="transition" :name="!drag ? 'flip-list' : null">
|
||||
<draggable v-model="filteredAccounts" @start="drag = true" @end="saveOrder" ghost-class="ghost" handle=".tfa-dots" animation="200" class="accounts">
|
||||
<transition-group class="columns is-multiline is-centered" type="transition" :name="!drag ? 'flip-list' : null">
|
||||
<div class="tfa column is-narrow has-text-white" v-for="account in filteredAccounts" :key="account.id">
|
||||
<div class="tfa-container">
|
||||
<transition name="slideCheckbox">
|
||||
@ -69,42 +70,17 @@
|
||||
</draggable>
|
||||
<!-- </vue-pull-refresh> -->
|
||||
</div>
|
||||
<!-- No account -->
|
||||
<div class="container has-text-centered" v-show="showQuickForm">
|
||||
<div class="columns is-mobile" :class="{ 'is-invisible' : this.accounts.length > 0}">
|
||||
<div class="column quickform-header">
|
||||
{{ $t('twofaccounts.no_account_here') }}<br>
|
||||
{{ $t('twofaccounts.add_first_account') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<form @submit.prevent="createAccount" @keydown="form.onKeydown($event)">
|
||||
<div class="columns is-mobile no-account is-vcentered">
|
||||
<div class="column has-text-centered">
|
||||
<label :class="{'is-loading' : form.isBusy}" class="button is-link is-medium is-rounded is-focused">
|
||||
<input class="file-input" type="file" accept="image/*" v-on:change="uploadQrcode" ref="qrcodeInput">
|
||||
{{ $t('twofaccounts.forms.use_qrcode.val') }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<field-error :form="form" field="qrcode" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="columns is-mobile">
|
||||
<div class="column quickform-footer">
|
||||
<router-link :to="{ name: 'create' }" class="is-link">{{ $t('twofaccounts.use_full_form') }}</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Show uploader (because no account) -->
|
||||
<quick-uploader v-if="showUploader" :directStreaming="accounts.length > 0" :showTrailer="accounts.length === 0" ref="QuickUploader"></quick-uploader>
|
||||
<!-- modal -->
|
||||
<modal v-model="ShowTwofaccountInModal">
|
||||
<modal v-model="showTwofaccountInModal">
|
||||
<twofaccount-show ref="TwofaccountShow" ></twofaccount-show>
|
||||
</modal>
|
||||
<!-- footer -->
|
||||
<vue-footer :showButtons="this.accounts.length > 0">
|
||||
<vue-footer v-if="showFooter" :showButtons="accounts.length > 0">
|
||||
<!-- New item buttons -->
|
||||
<p class="control" v-if="!showQuickForm && !editMode">
|
||||
<a class="button is-link is-rounded is-focus" @click="showQuickForm = true">
|
||||
<p class="control" v-if="!showUploader && !editMode">
|
||||
<a class="button is-link is-rounded is-focus" @click="showUploader = true">
|
||||
<span>{{ $t('twofaccounts.new') }}</span>
|
||||
<span class="icon is-small">
|
||||
<font-awesome-icon :icon="['fas', 'qrcode']" />
|
||||
@ -112,11 +88,11 @@
|
||||
</a>
|
||||
</p>
|
||||
<!-- Manage button -->
|
||||
<p class="control" v-if="!showQuickForm && !editMode">
|
||||
<p class="control" v-if="!showUploader && !editMode">
|
||||
<a class="button is-dark is-rounded" @click="setEditModeTo(true)">{{ $t('twofaccounts.manage') }}</a>
|
||||
</p>
|
||||
<!-- Done button -->
|
||||
<p class="control" v-if="!showQuickForm && editMode">
|
||||
<p class="control" v-if="!showUploader && editMode">
|
||||
<a class="button is-success is-rounded" @click="setEditModeTo(false)">
|
||||
<span>{{ $t('twofaccounts.done') }}</span>
|
||||
<span class="icon is-small">
|
||||
@ -125,8 +101,8 @@
|
||||
</a>
|
||||
</p>
|
||||
<!-- Cancel QuickFormButton -->
|
||||
<p class="control" v-if="showQuickForm">
|
||||
<a class="button is-dark is-rounded" @click="cancelQuickForm">
|
||||
<p class="control" v-if="showUploader && showFooter">
|
||||
<a class="button is-dark is-rounded" @click="showUploader = false">
|
||||
{{ $t('commons.cancel') }}
|
||||
</a>
|
||||
</p>
|
||||
@ -139,22 +115,21 @@
|
||||
|
||||
import Modal from '../components/Modal'
|
||||
import TwofaccountShow from '../components/TwofaccountShow'
|
||||
import Form from './../components/Form'
|
||||
import vuePullRefresh from 'vue-pull-refresh';
|
||||
import QuickUploader from './../components/QuickUploader'
|
||||
// import vuePullRefresh from 'vue-pull-refresh';
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
accounts : [],
|
||||
selectedAccounts: [],
|
||||
ShowTwofaccountInModal : false,
|
||||
showTwofaccountInModal : false,
|
||||
search: '',
|
||||
editMode: this.InitialEditMode,
|
||||
showQuickForm: false,
|
||||
form: new Form({
|
||||
qrcode: null
|
||||
}),
|
||||
showUploader: false,
|
||||
showFooter: true,
|
||||
drag: false,
|
||||
}
|
||||
},
|
||||
@ -174,13 +149,14 @@
|
||||
},
|
||||
|
||||
showAccounts() {
|
||||
return this.accounts.length > 0 && !this.showQuickForm ? true : false
|
||||
return this.accounts.length > 0 && !this.showUploader ? true : false
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
props: ['InitialEditMode'],
|
||||
|
||||
created() {
|
||||
mounted() {
|
||||
|
||||
this.fetchAccounts()
|
||||
|
||||
@ -190,39 +166,30 @@
|
||||
this.$refs.TwofaccountShow.clearOTP()
|
||||
});
|
||||
|
||||
// hide Footer when stream is on
|
||||
this.$on('initStreaming', function() {
|
||||
// this.showFooter = this.accounts.length > 0 ? false : true
|
||||
this.showFooter = false
|
||||
});
|
||||
|
||||
this.$on('stopStreaming', function() {
|
||||
|
||||
this.showUploader = this.accounts.length > 0 ? false : true
|
||||
this.showFooter = true
|
||||
});
|
||||
|
||||
},
|
||||
|
||||
components: {
|
||||
Modal,
|
||||
TwofaccountShow,
|
||||
'vue-pull-refresh': vuePullRefresh,
|
||||
// 'vue-pull-refresh': vuePullRefresh,
|
||||
QuickUploader,
|
||||
draggable,
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
||||
onRefresh() {
|
||||
var that = this
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
setTimeout(function () {
|
||||
that.fetchAccounts()
|
||||
resolve();
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
|
||||
async uploadQrcode(event) {
|
||||
|
||||
let imgdata = new FormData();
|
||||
imgdata.append('qrcode', this.$refs.qrcodeInput.files[0]);
|
||||
|
||||
const { data } = await this.form.upload('/api/qrcode/decode', imgdata)
|
||||
|
||||
this.$router.push({ name: 'create', params: { qrAccount: data } });
|
||||
|
||||
},
|
||||
|
||||
fetchAccounts() {
|
||||
this.accounts = []
|
||||
this.selectedAccounts = []
|
||||
@ -237,7 +204,7 @@
|
||||
})
|
||||
})
|
||||
|
||||
this.showQuickForm = response.data.length === 0 ? true: false
|
||||
this.showUploader = response.data.length === 0 ? true : false
|
||||
})
|
||||
},
|
||||
|
||||
@ -263,7 +230,7 @@
|
||||
this.axios.patch('/api/twofaccounts/reorder', {orderedIds: this.accounts.map(a => a.id)})
|
||||
},
|
||||
|
||||
deleteAccount: function (id) {
|
||||
deleteAccount(id) {
|
||||
if(confirm(this.$t('twofaccounts.confirm.delete'))) {
|
||||
this.axios.delete('/api/twofaccounts/' + id)
|
||||
|
||||
@ -299,10 +266,6 @@
|
||||
this.$parent.showToolbar = state
|
||||
},
|
||||
|
||||
cancelQuickForm() {
|
||||
this.form.clear()
|
||||
this.showQuickForm = false
|
||||
}
|
||||
},
|
||||
|
||||
beforeRouteEnter (to, from, next) {
|
||||
|
@ -8,6 +8,7 @@
|
||||
<form-select :options="options" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
|
||||
<form-switch :form="form" fieldName="showTokenAsDot" :label="$t('settings.forms.show_token_as_dot.label')" :help="$t('settings.forms.show_token_as_dot.help')" />
|
||||
<form-switch :form="form" fieldName="closeTokenOnCopy" :label="$t('settings.forms.close_token_on_copy.label')" :help="$t('settings.forms.close_token_on_copy.help')" />
|
||||
<form-switch :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
|
||||
</form>
|
||||
</form-wrapper>
|
||||
</template>
|
||||
@ -25,6 +26,7 @@
|
||||
lang: this.$root.$i18n.locale,
|
||||
showTokenAsDot: this.$root.appSettings.showTokenAsDot,
|
||||
closeTokenOnCopy: this.$root.appSettings.closeTokenOnCopy,
|
||||
useBasicQrcodeReader: this.$root.appSettings.useBasicQrcodeReader,
|
||||
}),
|
||||
options: [
|
||||
{ text: this.$t('languages.en'), value: 'en' },
|
||||
|
@ -35,6 +35,10 @@
|
||||
'label' => 'Close token after copy',
|
||||
'help' => 'Automatically close the popup showing the generated token after it has been copied'
|
||||
],
|
||||
'use_basic_qrcode_reader' => [
|
||||
'label' => 'Use basic qrcode reader',
|
||||
'help' => 'If you experiences issues when capturing qrCodes enables this option to switch to a more basic but more reliable qrcode reader'
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
'new' => 'New',
|
||||
'no_account_here' => 'No 2FA here!',
|
||||
'add_first_account' => 'Add your first account',
|
||||
'use_full_form' => 'Use the full form',
|
||||
'use_full_form' => 'Or use the full form',
|
||||
'add_one' => 'Add one',
|
||||
'manage' => 'Manage',
|
||||
'done' => 'Done',
|
||||
@ -34,6 +34,7 @@
|
||||
'edit_account' => 'Edit account',
|
||||
'otp_uri' => 'OTP Uri',
|
||||
'hotp_counter' => 'HOTP Counter',
|
||||
'scan_qrcode' => 'Scan a qrcode',
|
||||
'use_qrcode' => [
|
||||
'val' => 'Use a qrcode',
|
||||
'title' => 'Use a QR code to fill the form magically',
|
||||
@ -51,6 +52,14 @@
|
||||
'save' => 'Save',
|
||||
'test' => 'Test',
|
||||
],
|
||||
'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)',
|
||||
'camera_not_suitable' => 'Installed cameras are not suitable',
|
||||
'stream_api_not_supported' => 'Stream API is not supported in this browser'
|
||||
],
|
||||
'confirm' => [
|
||||
'delete' => 'Are you sure you want to delete this account?',
|
||||
'cancel' => 'The account will be lost. Are you sure?'
|
||||
|
@ -35,6 +35,10 @@
|
||||
'label' => 'Ne plus afficher les codes copiés',
|
||||
'help' => 'Ferme automatiquement le popup affichant le code généré dès que ce dernier a été copié.'
|
||||
],
|
||||
'use_basic_qrcode_reader' => [
|
||||
'label' => 'Utiliser le lecteur de qrcode basique',
|
||||
'help' => 'Si vous rencontrez des problèmes lors de la lecture des qrCodes activez cette option pour utiliser un lecteur de qrcode moins évolué mais plus largement compatible'
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
'new' => 'Nouveau',
|
||||
'no_account_here' => 'Aucun compte 2FA !',
|
||||
'add_first_account' => 'Ajouter votre premier compte',
|
||||
'use_full_form' => 'Utiliser le formulaire détaillé',
|
||||
'use_full_form' => 'Ou utiliser le formulaire détaillé',
|
||||
'add_one' => 'Add one',
|
||||
'manage' => 'Gérer',
|
||||
'done' => 'Terminé',
|
||||
@ -34,6 +34,7 @@
|
||||
'edit_account' => 'Modifier le compte',
|
||||
'otp_uri' => 'OTP Uri',
|
||||
'hotp_counter' => 'Compteur HOTP',
|
||||
'scan_qrcode' => 'Scanner un QR code',
|
||||
'use_qrcode' => [
|
||||
'val' => 'Utiliser un QR code',
|
||||
'title' => 'Utiliser un QR code pour renseigner le formulaire d\'un seul coup d\'un seul'
|
||||
@ -51,6 +52,14 @@
|
||||
'save' => 'Enregistrer',
|
||||
'test' => 'Tester',
|
||||
],
|
||||
'stream' => [
|
||||
'need_grant_permission' => 'Vous devez autoriser l\'utilisation de votre caméra',
|
||||
'not_readable' => 'Le scanner ne se charge pas. La caméra est-elle déjà utilisée ?',
|
||||
'no_cam_on_device' => 'Votre équipement ne dispose pas de caméra',
|
||||
'secured_context_required' => 'Contexte sécurisé requis (HTTPS ou localhost)',
|
||||
'camera_not_suitable' => 'Votre équipement ne dispose pas d\'une caméra adaptée',
|
||||
'stream_api_not_supported' => 'L\'API Stream n\'est pas supportée par votre navigateur'
|
||||
],
|
||||
'confirm' => [
|
||||
'delete' => 'Etes-vous sûrs de vouloir supprimer le compte ?',
|
||||
'cancel' => 'Les données seront perdues, êtes-vous sûrs ?'
|
||||
|
52
resources/sass/app.scss
vendored
52
resources/sass/app.scss
vendored
@ -1,6 +1,7 @@
|
||||
@import '~bulma';
|
||||
@import '~bulma-checkradio';
|
||||
@import '~bulma-switch';
|
||||
@import "~vue-qrcode-reader/dist/vue-qrcode-reader.css";
|
||||
|
||||
a:hover {
|
||||
color: hsl(204, 86%, 53%);
|
||||
@ -166,6 +167,30 @@ a:hover {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fullscreen-streamer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fullscreen-alert {
|
||||
position: fixed;
|
||||
top: 25vh;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.fullscreen-footer {
|
||||
position: fixed;
|
||||
top: calc(100vh - 8rem );
|
||||
left: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.has-ellipsis {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@ -319,27 +344,38 @@ footer .field.is-grouped {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.quickform-header {
|
||||
height: 20vh;
|
||||
padding-top: 2rem;
|
||||
.quick-uploader {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.quickform-footer {
|
||||
padding-top: 3rem;
|
||||
.quick-uploader-header {
|
||||
padding-top: 7vh;
|
||||
padding-bottom: 7vh;
|
||||
}
|
||||
|
||||
.preview {
|
||||
margin-top: 20vh;
|
||||
}
|
||||
|
||||
.no-account {
|
||||
.quick-uploader-button {
|
||||
height: 256px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.no-account::before {
|
||||
.quick-uploader-centerer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
height: 256px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quick-uploader-button::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
opacity: 0.05;
|
||||
|
Loading…
Reference in New Issue
Block a user