diff --git a/app/Classes/Options.php b/app/Classes/Options.php index 60c983ca..1049fb53 100644 --- a/app/Classes/Options.php +++ b/app/Classes/Options.php @@ -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; } diff --git a/app/Http/Controllers/QrCodeController.php b/app/Http/Controllers/QrCodeController.php index 63f9475d..66d66125 100644 --- a/app/Http/Controllers/QrCodeController.php +++ b/app/Http/Controllers/QrCodeController.php @@ -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 { diff --git a/config/app.php b/config/app.php index 6455224e..bb5dd642 100644 --- a/config/app.php +++ b/config/app.php @@ -35,6 +35,7 @@ 'isDemoApp' => env('IS_DEMO_APP', false), 'showTokenAsDot' => false, 'closeTokenOnCopy' => false, + 'useBasicQrcodeReader' => false, ], /* diff --git a/package-lock.json b/package-lock.json index 7fab1e9d..98b0f9ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 724a63b9..1e9caaf7 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/resources/js/app.js b/resources/js/app.js index 9ab6362a..4b3a6fa7 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -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' diff --git a/resources/js/components/QuickUploader.vue b/resources/js/components/QuickUploader.vue new file mode 100644 index 00000000..bd5b5ab0 --- /dev/null +++ b/resources/js/components/QuickUploader.vue @@ -0,0 +1,218 @@ + + + \ No newline at end of file diff --git a/resources/js/langs/locales.js b/resources/js/langs/locales.js index 652f7a57..a400f062 100644 --- a/resources/js/langs/locales.js +++ b/resources/js/langs/locales.js @@ -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 ?" diff --git a/resources/js/packages/fontawesome.js b/resources/js/packages/fontawesome.js index e57c2178..3a53d750 100644 --- a/resources/js/packages/fontawesome.js +++ b/resources/js/packages/fontawesome.js @@ -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) \ No newline at end of file diff --git a/resources/js/packages/qrcodeReader.js b/resources/js/packages/qrcodeReader.js new file mode 100644 index 00000000..8b09b879 --- /dev/null +++ b/resources/js/packages/qrcodeReader.js @@ -0,0 +1,4 @@ +import Vue from 'vue' +import QrcodeReader from 'vue-qrcode-reader' + +Vue.use(QrcodeReader) \ No newline at end of file diff --git a/resources/js/views/Accounts.vue b/resources/js/views/Accounts.vue index 5317f9ae..9e4a0107 100644 --- a/resources/js/views/Accounts.vue +++ b/resources/js/views/Accounts.vue @@ -1,5 +1,6 @@ @@ -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' }, diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index f4b609aa..10c007fc 100644 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -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' + ], ], diff --git a/resources/lang/en/twofaccounts.php b/resources/lang/en/twofaccounts.php index 641e5014..18b4ee22 100644 --- a/resources/lang/en/twofaccounts.php +++ b/resources/lang/en/twofaccounts.php @@ -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?' diff --git a/resources/lang/fr/settings.php b/resources/lang/fr/settings.php index 26dd0d48..9666ba61 100644 --- a/resources/lang/fr/settings.php +++ b/resources/lang/fr/settings.php @@ -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' + ], ], diff --git a/resources/lang/fr/twofaccounts.php b/resources/lang/fr/twofaccounts.php index 321c93c0..1f4347b2 100644 --- a/resources/lang/fr/twofaccounts.php +++ b/resources/lang/fr/twofaccounts.php @@ -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 ?' diff --git a/resources/sass/app.scss b/resources/sass/app.scss index c3051b9c..56fcf3f8 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -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;