diff --git a/composer.json b/composer.json
index 6fca6f0..67f3785 100644
--- a/composer.json
+++ b/composer.json
@@ -7,10 +7,11 @@
],
"license": "MIT",
"require": {
- "php": ">=7.1.0",
+ "php": ">=8.0.0",
"bacon/bacon-qr-code": "^2.0",
- "pragmarx/recovery": "^0.1.0",
- "pragmarx/google2fa-laravel": "^0.2.0"
+ "pragmarx/google2fa-laravel": "^2.0.1",
+ "pragmarx/google2fa-qrcode": "^3.0",
+ "pragmarx/recovery": "^0.2.0"
},
"autoload": {
"psr-4": {
diff --git a/config/lifeonscreen2fa.php b/config/lifeonscreen2fa.php
index d40dd13..6ae6413 100644
--- a/config/lifeonscreen2fa.php
+++ b/config/lifeonscreen2fa.php
@@ -41,13 +41,5 @@
* Number of characters in each block in recovery code.
*/
'chars_in_block' => 16,
-
- /**
- * The following algorithms are currently supported:
- * - PASSWORD_DEFAULT
- * - PASSWORD_BCRYPT
- * - PASSWORD_ARGON2I // available from php 7.2
- */
- 'hashing_algorithm' => PASSWORD_BCRYPT,
],
-];
\ No newline at end of file
+];
diff --git a/dist/css/tool.css b/dist/css/tool.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/dist/css/tool.css
@@ -0,0 +1 @@
+
diff --git a/dist/js/tool.js b/dist/js/tool.js
new file mode 100644
index 0000000..947b2e9
--- /dev/null
+++ b/dist/js/tool.js
@@ -0,0 +1,1297 @@
+/******/ (() => { // webpackBootstrap
+/******/ var __webpack_modules__ = ({
+
+/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/components/RecoveryCodesModal.vue?vue&type=script&lang=js&":
+/*!*************************************************************************************************************************************************************************************************************************!*\
+ !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/components/RecoveryCodesModal.vue?vue&type=script&lang=js& ***!
+ \*************************************************************************************************************************************************************************************************************************/
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
+/* harmony export */ });
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({
+ name: "RecoveryCodesModal",
+ data: function data() {
+ return {
+ codes: null,
+ confirming: false,
+ password: '',
+ errors: {}
+ };
+ },
+ methods: {
+ handleClose: function handleClose() {
+ this.$emit('close');
+ },
+ handleConfirm: function handleConfirm() {
+ this.$emit('confirm');
+ },
+ handleSubmit: function handleSubmit() {
+ var _this = this;
+
+ this.confirming = true;
+ this.errors = {};
+ Nova.request().post('/los/2fa/unlocked-recovery-codes', {
+ password: this.password
+ }).then(function (_ref) {
+ var data = _ref.data;
+ _this.codes = data.recovery_codes;
+ })["catch"](function (_ref2) {
+ var response = _ref2.response;
+ _this.errors = response.data.errors;
+ })["finally"](function () {
+ _this.confirming = false;
+ });
+ },
+ hideCodes: function hideCodes() {
+ this.codes = null;
+ this.handleClose();
+ }
+ },
+ computed: {
+ codesText: function codesText() {
+ return this.codes.join("\n");
+ }
+ },
+ mounted: function mounted() {
+ this.$refs.passwordInput.focus();
+ }
+});
+
+/***/ }),
+
+/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/components/Tool.vue?vue&type=script&lang=js&":
+/*!***********************************************************************************************************************************************************************************************************!*\
+ !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/components/Tool.vue?vue&type=script&lang=js& ***!
+ \***********************************************************************************************************************************************************************************************************/
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
+/* harmony export */ });
+/* harmony import */ var _RecoveryCodesModal_vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./RecoveryCodesModal.vue */ "./resources/js/components/RecoveryCodesModal.vue");
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+//
+
+/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({
+ components: {
+ RecoveryCodesModal: _RecoveryCodesModal_vue__WEBPACK_IMPORTED_MODULE_0__["default"]
+ },
+ metaInfo: function metaInfo() {
+ return {
+ title: 'Nova Two Factor'
+ };
+ },
+ data: function data() {
+ return {
+ showRecoveryCodesModal: false
+ };
+ },
+ mounted: function mounted() {//
+ },
+ methods: {
+ openModal: function openModal() {
+ this.showRecoveryCodesModal = true;
+ },
+ closeModal: function closeModal() {
+ this.showRecoveryCodesModal = false;
+ },
+ confirmModal: function confirmModal() {
+ this.closeModal();
+ }
+ }
+});
+
+/***/ }),
+
+/***/ "./resources/js/tool.js":
+/*!******************************!*\
+ !*** ./resources/js/tool.js ***!
+ \******************************/
+/***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
+
+Nova.booting(function (Vue, router, store) {
+ router.addRoutes([{
+ name: '2fa-recovery-codes',
+ path: '/recovery-codes',
+ component: (__webpack_require__(/*! ./components/Tool.vue */ "./resources/js/components/Tool.vue")["default"])
+ }]);
+});
+
+/***/ }),
+
+/***/ "./node_modules/css-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[1]!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[2]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/components/Tool.vue?vue&type=style&index=0&lang=css&":
+/*!*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
+ !*** ./node_modules/css-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[1]!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[2]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/components/Tool.vue?vue&type=style&index=0&lang=css& ***!
+ \*******************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
+/***/ ((module, __webpack_exports__, __webpack_require__) => {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
+/* harmony export */ });
+/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../../../node_modules/css-loader/dist/runtime/api.js */ "./node_modules/css-loader/dist/runtime/api.js");
+/* harmony import */ var _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0__);
+// Imports
+
+var ___CSS_LOADER_EXPORT___ = _node_modules_css_loader_dist_runtime_api_js__WEBPACK_IMPORTED_MODULE_0___default()(function(i){return i[1]});
+// Module
+___CSS_LOADER_EXPORT___.push([module.id, "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n/* Scoped Styles */\n", ""]);
+// Exports
+/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (___CSS_LOADER_EXPORT___);
+
+
+/***/ }),
+
+/***/ "./node_modules/css-loader/dist/runtime/api.js":
+/*!*****************************************************!*\
+ !*** ./node_modules/css-loader/dist/runtime/api.js ***!
+ \*****************************************************/
+/***/ ((module) => {
+
+"use strict";
+
+
+/*
+ MIT License http://www.opensource.org/licenses/mit-license.php
+ Author Tobias Koppers @sokra
+*/
+// css base code, injected by the css-loader
+// eslint-disable-next-line func-names
+module.exports = function (cssWithMappingToString) {
+ var list = []; // return the list of modules as css string
+
+ list.toString = function toString() {
+ return this.map(function (item) {
+ var content = cssWithMappingToString(item);
+
+ if (item[2]) {
+ return "@media ".concat(item[2], " {").concat(content, "}");
+ }
+
+ return content;
+ }).join("");
+ }; // import a list of modules into the list
+ // eslint-disable-next-line func-names
+
+
+ list.i = function (modules, mediaQuery, dedupe) {
+ if (typeof modules === "string") {
+ // eslint-disable-next-line no-param-reassign
+ modules = [[null, modules, ""]];
+ }
+
+ var alreadyImportedModules = {};
+
+ if (dedupe) {
+ for (var i = 0; i < this.length; i++) {
+ // eslint-disable-next-line prefer-destructuring
+ var id = this[i][0];
+
+ if (id != null) {
+ alreadyImportedModules[id] = true;
+ }
+ }
+ }
+
+ for (var _i = 0; _i < modules.length; _i++) {
+ var item = [].concat(modules[_i]);
+
+ if (dedupe && alreadyImportedModules[item[0]]) {
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ if (mediaQuery) {
+ if (!item[2]) {
+ item[2] = mediaQuery;
+ } else {
+ item[2] = "".concat(mediaQuery, " and ").concat(item[2]);
+ }
+ }
+
+ list.push(item);
+ }
+ };
+
+ return list;
+};
+
+/***/ }),
+
+/***/ "./resources/sass/tool.scss":
+/*!**********************************!*\
+ !*** ./resources/sass/tool.scss ***!
+ \**********************************/
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+// extracted by mini-css-extract-plugin
+
+
+/***/ }),
+
+/***/ "./node_modules/style-loader/dist/cjs.js!./node_modules/css-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[1]!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[2]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/components/Tool.vue?vue&type=style&index=0&lang=css&":
+/*!***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************!*\
+ !*** ./node_modules/style-loader/dist/cjs.js!./node_modules/css-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[1]!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[2]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/components/Tool.vue?vue&type=style&index=0&lang=css& ***!
+ \***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************/
+/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
+
+"use strict";
+__webpack_require__.r(__webpack_exports__);
+/* harmony export */ __webpack_require__.d(__webpack_exports__, {
+/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
+/* harmony export */ });
+/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! !../../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
+/* harmony import */ var _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0__);
+/* harmony import */ var _node_modules_css_loader_dist_cjs_js_clonedRuleSet_9_0_rules_0_use_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_dist_cjs_js_clonedRuleSet_9_0_rules_0_use_2_node_modules_vue_loader_lib_index_js_vue_loader_options_Tool_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! !!../../../node_modules/css-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[1]!../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[2]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./Tool.vue?vue&type=style&index=0&lang=css& */ "./node_modules/css-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[1]!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/postcss-loader/dist/cjs.js??clonedRuleSet-9[0].rules[0].use[2]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/components/Tool.vue?vue&type=style&index=0&lang=css&");
+
+
+
+var options = {};
+
+options.insert = "head";
+options.singleton = false;
+
+var update = _node_modules_style_loader_dist_runtime_injectStylesIntoStyleTag_js__WEBPACK_IMPORTED_MODULE_0___default()(_node_modules_css_loader_dist_cjs_js_clonedRuleSet_9_0_rules_0_use_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_dist_cjs_js_clonedRuleSet_9_0_rules_0_use_2_node_modules_vue_loader_lib_index_js_vue_loader_options_Tool_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_1__["default"], options);
+
+
+
+/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_css_loader_dist_cjs_js_clonedRuleSet_9_0_rules_0_use_1_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_postcss_loader_dist_cjs_js_clonedRuleSet_9_0_rules_0_use_2_node_modules_vue_loader_lib_index_js_vue_loader_options_Tool_vue_vue_type_style_index_0_lang_css___WEBPACK_IMPORTED_MODULE_1__["default"].locals || {});
+
+/***/ }),
+
+/***/ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js":
+/*!****************************************************************************!*\
+ !*** ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js ***!
+ \****************************************************************************/
+/***/ ((module, __unused_webpack_exports, __webpack_require__) => {
+
+"use strict";
+
+
+var isOldIE = function isOldIE() {
+ var memo;
+ return function memorize() {
+ if (typeof memo === 'undefined') {
+ // Test for IE <= 9 as proposed by Browserhacks
+ // @see http://browserhacks.com/#hack-e71d8692f65334173fee715c222cb805
+ // Tests for existence of standard globals is to allow style-loader
+ // to operate correctly into non-standard environments
+ // @see https://github.com/webpack-contrib/style-loader/issues/177
+ memo = Boolean(window && document && document.all && !window.atob);
+ }
+
+ return memo;
+ };
+}();
+
+var getTarget = function getTarget() {
+ var memo = {};
+ return function memorize(target) {
+ if (typeof memo[target] === 'undefined') {
+ var styleTarget = document.querySelector(target); // Special case to return head of iframe instead of iframe itself
+
+ if (window.HTMLIFrameElement && styleTarget instanceof window.HTMLIFrameElement) {
+ try {
+ // This will throw an exception if access to iframe is blocked
+ // due to cross-origin restrictions
+ styleTarget = styleTarget.contentDocument.head;
+ } catch (e) {
+ // istanbul ignore next
+ styleTarget = null;
+ }
+ }
+
+ memo[target] = styleTarget;
+ }
+
+ return memo[target];
+ };
+}();
+
+var stylesInDom = [];
+
+function getIndexByIdentifier(identifier) {
+ var result = -1;
+
+ for (var i = 0; i < stylesInDom.length; i++) {
+ if (stylesInDom[i].identifier === identifier) {
+ result = i;
+ break;
+ }
+ }
+
+ return result;
+}
+
+function modulesToDom(list, options) {
+ var idCountMap = {};
+ var identifiers = [];
+
+ for (var i = 0; i < list.length; i++) {
+ var item = list[i];
+ var id = options.base ? item[0] + options.base : item[0];
+ var count = idCountMap[id] || 0;
+ var identifier = "".concat(id, " ").concat(count);
+ idCountMap[id] = count + 1;
+ var index = getIndexByIdentifier(identifier);
+ var obj = {
+ css: item[1],
+ media: item[2],
+ sourceMap: item[3]
+ };
+
+ if (index !== -1) {
+ stylesInDom[index].references++;
+ stylesInDom[index].updater(obj);
+ } else {
+ stylesInDom.push({
+ identifier: identifier,
+ updater: addStyle(obj, options),
+ references: 1
+ });
+ }
+
+ identifiers.push(identifier);
+ }
+
+ return identifiers;
+}
+
+function insertStyleElement(options) {
+ var style = document.createElement('style');
+ var attributes = options.attributes || {};
+
+ if (typeof attributes.nonce === 'undefined') {
+ var nonce = true ? __webpack_require__.nc : 0;
+
+ if (nonce) {
+ attributes.nonce = nonce;
+ }
+ }
+
+ Object.keys(attributes).forEach(function (key) {
+ style.setAttribute(key, attributes[key]);
+ });
+
+ if (typeof options.insert === 'function') {
+ options.insert(style);
+ } else {
+ var target = getTarget(options.insert || 'head');
+
+ if (!target) {
+ throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");
+ }
+
+ target.appendChild(style);
+ }
+
+ return style;
+}
+
+function removeStyleElement(style) {
+ // istanbul ignore if
+ if (style.parentNode === null) {
+ return false;
+ }
+
+ style.parentNode.removeChild(style);
+}
+/* istanbul ignore next */
+
+
+var replaceText = function replaceText() {
+ var textStore = [];
+ return function replace(index, replacement) {
+ textStore[index] = replacement;
+ return textStore.filter(Boolean).join('\n');
+ };
+}();
+
+function applyToSingletonTag(style, index, remove, obj) {
+ var css = remove ? '' : obj.media ? "@media ".concat(obj.media, " {").concat(obj.css, "}") : obj.css; // For old IE
+
+ /* istanbul ignore if */
+
+ if (style.styleSheet) {
+ style.styleSheet.cssText = replaceText(index, css);
+ } else {
+ var cssNode = document.createTextNode(css);
+ var childNodes = style.childNodes;
+
+ if (childNodes[index]) {
+ style.removeChild(childNodes[index]);
+ }
+
+ if (childNodes.length) {
+ style.insertBefore(cssNode, childNodes[index]);
+ } else {
+ style.appendChild(cssNode);
+ }
+ }
+}
+
+function applyToTag(style, options, obj) {
+ var css = obj.css;
+ var media = obj.media;
+ var sourceMap = obj.sourceMap;
+
+ if (media) {
+ style.setAttribute('media', media);
+ } else {
+ style.removeAttribute('media');
+ }
+
+ if (sourceMap && typeof btoa !== 'undefined') {
+ css += "\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), " */");
+ } // For old IE
+
+ /* istanbul ignore if */
+
+
+ if (style.styleSheet) {
+ style.styleSheet.cssText = css;
+ } else {
+ while (style.firstChild) {
+ style.removeChild(style.firstChild);
+ }
+
+ style.appendChild(document.createTextNode(css));
+ }
+}
+
+var singleton = null;
+var singletonCounter = 0;
+
+function addStyle(obj, options) {
+ var style;
+ var update;
+ var remove;
+
+ if (options.singleton) {
+ var styleIndex = singletonCounter++;
+ style = singleton || (singleton = insertStyleElement(options));
+ update = applyToSingletonTag.bind(null, style, styleIndex, false);
+ remove = applyToSingletonTag.bind(null, style, styleIndex, true);
+ } else {
+ style = insertStyleElement(options);
+ update = applyToTag.bind(null, style, options);
+
+ remove = function remove() {
+ removeStyleElement(style);
+ };
+ }
+
+ update(obj);
+ return function updateStyle(newObj) {
+ if (newObj) {
+ if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap) {
+ return;
+ }
+
+ update(obj = newObj);
+ } else {
+ remove();
+ }
+ };
+}
+
+module.exports = function (list, options) {
+ options = options || {}; // Force single-tag solution on IE6-9, which has a hard limit on the # of
diff --git a/resources/js/components/Tool.vue b/resources/js/components/Tool.vue
new file mode 100644
index 0000000..fde2224
--- /dev/null
+++ b/resources/js/components/Tool.vue
@@ -0,0 +1,68 @@
+
+
+ Every time you use a recovery code, that one gets removed from your list of recovery codes and a new one is generated to take its place.
+
+ Recovery Codes
+
+
+
Enter the pin from Google Authenticator Enable 2FA
+Enter the pin from your Authenticator app
{{ $error }} -
@endif +