mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-02-23 13:51:13 +01:00
Merge branch 'release/1.1.0'
This commit is contained in:
commit
46cd2049aa
@ -32,6 +32,12 @@ APP_KEY=SomeRandomStringOf32CharsExactly
|
||||
APP_URL=http://localhost
|
||||
|
||||
|
||||
# Turn this to true if you want your app to react like a demo.
|
||||
# The Demo mode reset the app content every hours and set a generic demo user.
|
||||
|
||||
IS_DEMO_APP=false
|
||||
|
||||
|
||||
# The log channel defines where your log entries go to.
|
||||
# 'daily' is the default logging mode giving you 5 daily rotated log files in /storage/logs/.
|
||||
# Several other options exist. You can use 'single' for one big fat error log (not recommended).
|
||||
|
57
app/Classes/Options.php
Normal file
57
app/Classes/Options.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Classes;
|
||||
|
||||
class Options
|
||||
{
|
||||
|
||||
/**
|
||||
* Build a collection of options to apply
|
||||
*
|
||||
* @return Options collection
|
||||
*/
|
||||
public static function get()
|
||||
{
|
||||
// Get a collection of user saved options
|
||||
$userOptions = \Illuminate\Support\Facades\DB::table('options')->pluck('value', 'key');
|
||||
|
||||
// We replace patterned string that represent booleans with real booleans
|
||||
$userOptions->transform(function ($item, $key) {
|
||||
if( $item === '{{}}' ) {
|
||||
return false;
|
||||
}
|
||||
else if( $item === '{{1}}' ) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return $item;
|
||||
}
|
||||
});
|
||||
|
||||
// Merge options from App configuration. It ensures we have a complete options collection with
|
||||
// fallback values for every options
|
||||
$options = collect(config('app.options'))->merge($userOptions);
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set user options
|
||||
*
|
||||
* @param array All options to store
|
||||
* @return void
|
||||
*/
|
||||
public static function store($userOptions)
|
||||
{
|
||||
foreach($userOptions as $opt => $val) {
|
||||
|
||||
// We replace boolean values by a patterned string in order to retrieve
|
||||
// them later (as the Laravel Options package do not support var type)
|
||||
// Not a beatufilly solution but, hey, it works ^_^
|
||||
option([$opt => is_bool($val) ? '{{' . $val . '}}' : $val]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
120
app/Console/Commands/ResetDemo.php
Normal file
120
app/Console/Commands/ResetDemo.php
Normal file
File diff suppressed because one or more lines are too long
@ -41,7 +41,9 @@ public function update(Request $request)
|
||||
return response()->json(['message' => __('errors.wrong_current_password')], 400);
|
||||
}
|
||||
|
||||
tap($user)->update($request->only('name', 'email'));
|
||||
if (!config('app.options.isDemoApp') ) {
|
||||
tap($user)->update($request->only('name', 'email'));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'message' => __('auth.forms.profile_saved'),
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\Classes\Options;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
class OptionController extends Controller
|
||||
@ -17,7 +17,7 @@ class OptionController extends Controller
|
||||
public function index()
|
||||
{
|
||||
// Fetch all setting values
|
||||
$settings = DB::table('options')->get();
|
||||
$settings = Options::get();
|
||||
|
||||
return response()->json(['settings' => $settings], 200);
|
||||
}
|
||||
@ -29,12 +29,9 @@ public function index()
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
// Store all setting values
|
||||
foreach($request->all() as $opt => $val) {
|
||||
option([$opt => $val]);
|
||||
$settings[$opt] = option($opt);
|
||||
}
|
||||
// Store all options
|
||||
Options::store($request->all());
|
||||
|
||||
return response()->json(['message' => __('settings.forms.setting_saved'), 'settings' => $settings], 200);
|
||||
return response()->json(['message' => __('settings.forms.setting_saved'), 'settings' => Options::get()], 200);
|
||||
}
|
||||
}
|
||||
|
@ -27,9 +27,11 @@ public function update(Request $request)
|
||||
return response()->json(['message' => __('errors.wrong_current_password')], 400);
|
||||
}
|
||||
|
||||
$request->user()->update([
|
||||
'password' => bcrypt($request->password),
|
||||
]);
|
||||
if (!config('app.options.isDemoApp') ) {
|
||||
$request->user()->update([
|
||||
'password' => bcrypt($request->password),
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => __('auth.forms.password_successfully_changed')]);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Classes\Options;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SinglePageController extends Controller
|
||||
@ -13,8 +14,6 @@ class SinglePageController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$appSettings = \Illuminate\Support\Facades\DB::table('options')->pluck('value', 'key')->toJson();
|
||||
|
||||
return view('landing')->with('appSettings', $appSettings);
|
||||
return view('landing')->with('appSettings', Options::get()->toJson());
|
||||
}
|
||||
}
|
||||
|
12
changelog.md
Normal file
12
changelog.md
Normal file
@ -0,0 +1,12 @@
|
||||
## [1.1.0] - 2020-03-23
|
||||
|
||||
### Added
|
||||
- Demonstration mode with restricted features and ability to reset content with an artisan command
|
||||
- Option to close token popup when the code is pasted (by clicking/taping on it)
|
||||
|
||||
### Changed
|
||||
- Options default values can now be set in config/app
|
||||
- Generated assets are now part of the repo to ease deployement
|
||||
|
||||
### Fixed
|
||||
- Option labels attached to wrong checkboxes
|
@ -22,7 +22,20 @@
|
||||
|
|
||||
*/
|
||||
|
||||
'version' => '1.0.0',
|
||||
'version' => '1.1.0',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Application fallback for user options
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
'options' => [
|
||||
'isDemoApp' => env('IS_DEMO_APP', false),
|
||||
'showTokenAsDot' => false,
|
||||
'closeTokenOnCopy' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
85
database/seeds/DemoSeeder.php
Normal file
85
database/seeds/DemoSeeder.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
use App\User;
|
||||
use App\TwoFAccount;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class DemoSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
User::create([
|
||||
'name' => 'demo',
|
||||
'email' => 'demo@2fauth.app',
|
||||
'password' => bcrypt('demo'),
|
||||
]);
|
||||
|
||||
TwoFAccount::create([
|
||||
'service' => 'Amazon',
|
||||
'account' => 'johndoe',
|
||||
'uri' => 'otpauth://totp/johndoe@amazon.com?secret=A7GRFTVVRBGY7UIW&issuer=amazon',
|
||||
'icon' => 'amazon.png'
|
||||
]);
|
||||
|
||||
TwoFAccount::create([
|
||||
'service' => 'Apple',
|
||||
'account' => 'john.doe@icloud.com',
|
||||
'uri' => 'otpauth://totp/john@apple.com?secret=A2GRFTVVRBGY7UIW&issuer=apple',
|
||||
'icon' => 'apple.png'
|
||||
]);
|
||||
|
||||
TwoFAccount::create([
|
||||
'service' => 'Dropbox',
|
||||
'account' => 'john.doe',
|
||||
'uri' => 'otpauth://totp/johndoe@dropbox.com?secret=A3GRFTVVRBGY7UIW&issuer=dropbox',
|
||||
'icon' => 'dropbox.png'
|
||||
]);
|
||||
|
||||
TwoFAccount::create([
|
||||
'service' => 'Facebook',
|
||||
'account' => 'johndoe@facebook.com',
|
||||
'uri' => 'otpauth://totp/johndoe@facebook.com?secret=A4GRFTVVRBGY7UIW&issuer=facebook',
|
||||
'icon' => 'facebook.png'
|
||||
]);
|
||||
|
||||
TwoFAccount::create([
|
||||
'service' => 'Github',
|
||||
'account' => '@john',
|
||||
'uri' => 'otpauth://totp/johndoe@github.com?secret=A2GRFTVVRBGY7UIW&issuer=github',
|
||||
'icon' => 'github.png'
|
||||
]);
|
||||
|
||||
TwoFAccount::create([
|
||||
'service' => 'Google',
|
||||
'account' => 'john.doe@gmail.com',
|
||||
'uri' => 'otpauth://totp/johndoe@google.com?secret=A5GRFTVVRBGY7UIW&issuer=google',
|
||||
'icon' => 'google.png'
|
||||
]);
|
||||
|
||||
TwoFAccount::create([
|
||||
'service' => 'Instagram',
|
||||
'account' => '@johndoe',
|
||||
'uri' => 'otpauth://totp/johndoe@instagram.com?secret=A6GRFTVVRBGY7UIW&issuer=instagram',
|
||||
'icon' => 'instagram.png'
|
||||
]);
|
||||
|
||||
TwoFAccount::create([
|
||||
'service' => 'LinkedIn',
|
||||
'account' => '@johndoe',
|
||||
'uri' => 'otpauth://totp/johndoe@linkedin.com?secret=A7GRFTVVRBGY7UIW&issuer=linkedin',
|
||||
'icon' => 'linkedin.png'
|
||||
]);
|
||||
|
||||
TwoFAccount::create([
|
||||
'service' => 'Twitter',
|
||||
'account' => '@john',
|
||||
'uri' => 'otpauth://totp/johndoe@twitter.com?secret=A2GRFTVVRBGY7UIW&issuer=twitter',
|
||||
'icon' => 'twitter.png'
|
||||
]);
|
||||
}
|
||||
}
|
74
package-lock.json
generated
74
package-lock.json
generated
@ -827,40 +827,40 @@
|
||||
}
|
||||
},
|
||||
"@fortawesome/fontawesome-common-types": {
|
||||
"version": "0.2.26",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.26.tgz",
|
||||
"integrity": "sha512-CcM/fIFwZlRdiWG/25xE/wHbtyUuCtqoCTrr6BsWw7hH072fR++n4L56KPydAr3ANgMJMjT8v83ZFIsDc7kE+A=="
|
||||
"version": "0.2.27",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.27.tgz",
|
||||
"integrity": "sha512-97GaByGaXDGMkzcJX7VmR/jRJd8h1mfhtA7RsxDBN61GnWE/PPCZhOdwG/8OZYktiRUF0CvFOr+VgRkJrt6TWg=="
|
||||
},
|
||||
"@fortawesome/fontawesome-svg-core": {
|
||||
"version": "1.2.26",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.26.tgz",
|
||||
"integrity": "sha512-3Dfd/v2IztP1TxKOxZiB5+4kaOZK9mNy0KU1vVK7nFlPWz3gzxrCWB+AloQhQUoJ8HhGqbzjliK89Vl7PExGbw==",
|
||||
"version": "1.2.27",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-1.2.27.tgz",
|
||||
"integrity": "sha512-sOD3DKynocnHYpuw2sLPnTunDj7rLk91LYhi2axUYwuGe9cPCw7Bsu9EWtVdNJP+IYgTCZIbyARKXuy5K/nv+Q==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.26"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.27"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-brands-svg-icons": {
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.12.0.tgz",
|
||||
"integrity": "sha512-50uCFzVUki3wfmFmrMNLFhOt8dP6YZ53zwR4dK9FR7Lwq1IVHXnSBb8MtGLe3urLJ2sA+CSu7Pc7s3i6/zLxmA==",
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-5.12.1.tgz",
|
||||
"integrity": "sha512-IYUYcgGsQuwiIHjRGfeSTCIQKUSZMb6FsV6mDj78K0D+YzGJkM4cvEBBUMHtnla5D2HCxncMI/9JX5YIk2GHeQ==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.26"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.27"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-regular-svg-icons": {
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.12.0.tgz",
|
||||
"integrity": "sha512-FAvpmylTs0PosHwHrWPQX6/7ODc9M11kCE6AOAujFufDYzqTj2cPHT4yJO7zTEkKdAbbusJzbWpnOboMuyjeQA==",
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-5.12.1.tgz",
|
||||
"integrity": "sha512-bGda18seHXb+24K6DPUFzqn4kG7B+JViP/BscMcNUXvT00M86xNhdgP2TXSdflQXn53QWqymKjx/8rhaDOJyhA==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.26"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.27"
|
||||
}
|
||||
},
|
||||
"@fortawesome/free-solid-svg-icons": {
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.12.0.tgz",
|
||||
"integrity": "sha512-CnpsWs6GhTs9ekNB3d8rcO5HYqRkXbYKf2YNiAlTWbj5eVlPqsd/XH1F9If8jkcR1aegryAbln/qYeKVZzpM0g==",
|
||||
"version": "5.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-5.12.1.tgz",
|
||||
"integrity": "sha512-k3MwRFFUhyL4cuCJSaHDA0YNYMELDXX0h8JKtWYxO5XD3Dn+maXOMrVAAiNGooUyM2v/wz/TOaM0jxYVKeXX7g==",
|
||||
"requires": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.26"
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.27"
|
||||
}
|
||||
},
|
||||
"@fortawesome/vue-fontawesome": {
|
||||
@ -1176,9 +1176,9 @@
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
|
||||
"integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==",
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
|
||||
"integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
|
||||
"dev": true
|
||||
},
|
||||
"adjust-sourcemap-loader": {
|
||||
@ -5453,9 +5453,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"kind-of": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
|
||||
"integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"dev": true
|
||||
},
|
||||
"laravel-mix": {
|
||||
@ -6604,9 +6604,9 @@
|
||||
}
|
||||
},
|
||||
"popper.js": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.0.tgz",
|
||||
"integrity": "sha512-+G+EkOPoE5S/zChTpmBSSDYmhXJ5PsW8eMhH8cP/CQHMFPBG/kC9Y5IIw6qNYgdJ+/COf0ddY2li28iHaZRSjw==",
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
|
||||
"integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==",
|
||||
"dev": true
|
||||
},
|
||||
"portfinder": {
|
||||
@ -7847,9 +7847,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.24.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.24.2.tgz",
|
||||
"integrity": "sha512-0JxdMMRd0fOmGFQFRI91vh4n0Ed766ib9JwPUa+1C37zn3VaqlHxbknUn/6LqP/MSfvNPxRYoCrYf5g8vu4OHw==",
|
||||
"version": "1.26.3",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.26.3.tgz",
|
||||
"integrity": "sha512-5NMHI1+YFYw4sN3yfKjpLuV9B5l7MqQ6FlkTcC4FT+oHbBRUZoSjHrrt/mE0nFXJyY2kQtU9ou9HxvFVjLFuuw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=2.0.0 <4.0.0"
|
||||
@ -9167,9 +9167,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"vue-i18n": {
|
||||
"version": "8.15.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.15.3.tgz",
|
||||
"integrity": "sha512-PVNgo6yhOmacZVFjSapZ314oewwLyXHjJwAqjnaPN1GJAJd/dvsrShGzSiJuCX4Hc36G4epJvNXUwO8y7wEKew=="
|
||||
"version": "8.15.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.15.5.tgz",
|
||||
"integrity": "sha512-lIej02+w8lP0k1PEN1xtXqKpQ1hDh17zvDF+7Oc2qJi+cTMDlfPM771w4euVaHO67AxEz4WL9MIgkyn3tkeCtQ=="
|
||||
},
|
||||
"vue-loader": {
|
||||
"version": "15.8.3",
|
||||
@ -9193,9 +9193,9 @@
|
||||
}
|
||||
},
|
||||
"vue-router": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.3.tgz",
|
||||
"integrity": "sha512-8iSa4mGNXBjyuSZFCCO4fiKfvzqk+mhL0lnKuGcQtO1eoj8nq3CmbEG8FwK5QqoqwDgsjsf1GDuisDX4cdb/aQ=="
|
||||
"version": "3.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.1.6.tgz",
|
||||
"integrity": "sha512-GYhn2ynaZlysZMkFE5oCHRUTqE8BWs/a9YbKpNLi0i7xD6KG1EzDqpHQmv1F5gXjr8kL5iIVS8EOtRaVUEXTqA=="
|
||||
},
|
||||
"vue-style-loader": {
|
||||
"version": "4.1.2",
|
||||
|
16
package.json
16
package.json
@ -17,25 +17,25 @@
|
||||
"jquery": "^3.2",
|
||||
"laravel-mix": "^4.1.4",
|
||||
"lodash": "^4.17.15",
|
||||
"popper.js": "^1.16.0",
|
||||
"popper.js": "^1.16.1",
|
||||
"resolve-url-loader": "^2.3.1",
|
||||
"sass": "^1.24.2",
|
||||
"sass": "^1.26.3",
|
||||
"sass-loader": "^7.3.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.26",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.12.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.12.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.27",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.12.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^5.12.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.12.1",
|
||||
"@fortawesome/vue-fontawesome": "^0.1.9",
|
||||
"bulma-checkradio": "^1.1.1",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"v-clipboard": "^2.2.2",
|
||||
"vue-axios": "^2.1.5",
|
||||
"vue-i18n": "^8.15.3",
|
||||
"vue-i18n": "^8.15.5",
|
||||
"vue-pull-refresh": "^0.2.7",
|
||||
"vue-router": "^3.1.3"
|
||||
"vue-router": "^3.1.6"
|
||||
}
|
||||
}
|
||||
|
3
public/css/app.css
vendored
Normal file
3
public/css/app.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/js/app.js
vendored
Normal file
1
public/js/app.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/js/locales.js
vendored
Normal file
1
public/js/locales.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
public/js/manifest.js
vendored
Normal file
1
public/js/manifest.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
!function(e){function r(r){for(var n,l,f=r[0],i=r[1],a=r[2],c=0,s=[];c<f.length;c++)l=f[c],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in i)Object.prototype.hasOwnProperty.call(i,n)&&(e[n]=i[n]);for(p&&p(r);s.length;)s.shift()();return u.push.apply(u,a||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,f=1;f<t.length;f++){var i=t[f];0!==o[i]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={0:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var f=window.webpackJsonp=window.webpackJsonp||[],i=f.push.bind(f);f.push=r,f=f.slice();for(var a=0;a<f.length;a++)r(f[a]);var p=i;t()}([]);
|
1
public/js/vendor.js
vendored
Normal file
1
public/js/vendor.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
public/mix-manifest.json
Normal file
7
public/mix-manifest.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"/js/manifest.js": "/js/manifest.js?id=7db827d654313dce4250",
|
||||
"/js/app.js": "/js/app.js?id=80de66960444f655531d",
|
||||
"/css/app.css": "/css/app.css?id=46032e0e6368c4c5cfcc",
|
||||
"/js/locales.js": "/js/locales.js?id=63696a94f3a7b1fe09d6",
|
||||
"/js/vendor.js": "/js/vendor.js?id=1cd1d953565ebfcb7231"
|
||||
}
|
4
resources/js/app.js
vendored
4
resources/js/app.js
vendored
@ -10,6 +10,10 @@ import './components'
|
||||
|
||||
const app = new Vue({
|
||||
el: '#app',
|
||||
data: {
|
||||
appSettings: window.appSettings,
|
||||
appVersion: window.appVersion
|
||||
},
|
||||
components: { App },
|
||||
i18n,
|
||||
router,
|
||||
|
@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<main class="main-section">
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
<div>
|
||||
<div v-if="$root.appSettings.isDemoApp" class="demo has-background-warning has-text-centered is-size-7-mobile">
|
||||
{{ $t('commons.demo_do_not_post_sensitive_data') }}
|
||||
</div>
|
||||
<main class="main-section">
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="field">
|
||||
<label for="fieldName" class="label" v-html="label"></label>
|
||||
<input id="fieldName" type="checkbox" name="fieldName" class="switch is-thin is-info" v-model="form[fieldName]">
|
||||
<label for="fieldName" class="label"></label>
|
||||
<label :for="fieldName" class="label" v-html="label"></label>
|
||||
<input :id="fieldName" type="checkbox" :name="fieldName" class="switch is-thin is-info" v-model="form[fieldName]">
|
||||
<label :for="fieldName" class="label"></label>
|
||||
<p class="help" v-html="help" v-if="help"></p>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -42,7 +42,7 @@
|
||||
|
||||
computed: {
|
||||
displayedOtp() {
|
||||
return Boolean(Number(appSettings.showTokenAsDot)) ? this.otp.replace(/[0-9]/g, '●') : this.otp
|
||||
return this.$root.appSettings.showTokenAsDot ? this.otp.replace(/[0-9]/g, '●') : this.otp
|
||||
}
|
||||
},
|
||||
|
||||
@ -163,6 +163,11 @@
|
||||
|
||||
clipboardSuccessHandler ({ value, event }) {
|
||||
console.log('success', value)
|
||||
|
||||
if(this.$root.appSettings.closeTokenOnCopy) {
|
||||
this.$parent.isActive = false
|
||||
this.clearOTP()
|
||||
}
|
||||
},
|
||||
|
||||
clipboardErrorHandler ({ value, event }) {
|
||||
|
24
resources/js/langs/locales.js
vendored
24
resources/js/langs/locales.js
vendored
@ -33,7 +33,8 @@ export default {
|
||||
"change_your_password": "Change your password",
|
||||
"password_successfully_changed": "Password successfully changed ",
|
||||
"edit_account": "Edit account",
|
||||
"profile_saved": "Profile successfully updated!"
|
||||
"profile_saved": "Profile successfully updated!",
|
||||
"welcome_to_demo_app_use_those_credentials": "Welcome to the 2FAuth demo.<br><br>You can connect using the email address <strong>demo@2fauth.app</strong> and the password <strong>demo</demo>"
|
||||
}
|
||||
},
|
||||
"commons": {
|
||||
@ -44,7 +45,8 @@ export default {
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"close": "Close"
|
||||
"close": "Close",
|
||||
"demo_do_not_post_sensitive_data": "This is a demo app, do not post any sensitive data"
|
||||
},
|
||||
"errors": {
|
||||
"resource_not_found": "Resource not found",
|
||||
@ -91,6 +93,10 @@ export default {
|
||||
"show_token_as_dot": {
|
||||
"label": "Show generated tokens as dot",
|
||||
"help": "Replace generated token caracters with *** to ensure confidentiality. Do not affect the copy/paste feature."
|
||||
},
|
||||
"close_token_on_copy": {
|
||||
"label": "Close token after copy",
|
||||
"help": "Automatically close the popup showing the generated token after it has been copied"
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -296,7 +302,8 @@ export default {
|
||||
"change_your_password": "Modifier votre mot de passe",
|
||||
"password_successfully_changed": "Mot de passe modifié avec succès",
|
||||
"edit_account": "Mis à jour du profil",
|
||||
"profile_saved": "Profil mis à jour avec succès !"
|
||||
"profile_saved": "Profil mis à jour avec succès !",
|
||||
"welcome_to_demo_app_use_those_credentials": "bienvenue sur la démo de 2FAuth.<br><br>Vous pouvez vous connecter en utilisant l'adresse email <strong>demo@2fauth.app</strong> et le mot de passe <strong>demo</demo>"
|
||||
}
|
||||
},
|
||||
"commons": {
|
||||
@ -307,7 +314,8 @@ export default {
|
||||
"edit": "Modifier",
|
||||
"delete": "Supprimer",
|
||||
"save": "Enregistrer",
|
||||
"close": "Fermer"
|
||||
"close": "Fermer",
|
||||
"demo_do_not_post_sensitive_data": "Site de démonstration, ne postez aucune donnée sensible"
|
||||
},
|
||||
"errors": {
|
||||
"resource_not_found": "Ressource introuvable",
|
||||
@ -352,8 +360,12 @@ export default {
|
||||
"help": "Traduit l'application dans la langue choisie"
|
||||
},
|
||||
"show_token_as_dot": {
|
||||
"label": "Masquer les codes générés",
|
||||
"help": "Remplace les caractères des codes générés par des *** pour garantir leur confidentialité. N'affecte pas la fonction copier/coller qui reste utilisable."
|
||||
"label": "Rendre illisibles les codes générés",
|
||||
"help": "Remplace les caractères des codes générés par des ●●● pour garantir leur confidentialité. N'affecte pas la fonction de copier/coller qui reste utilisable."
|
||||
},
|
||||
"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é."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<form-wrapper :title="$t('auth.forms.login')" :fail="fail" :success="success">
|
||||
<div v-if="$root.appSettings.isDemoApp" class="notification is-info has-text-centered" v-html="$t('auth.forms.welcome_to_demo_app_use_those_credentials')" />
|
||||
<form @submit.prevent="handleSubmit" @keydown="form.onKeydown($event)">
|
||||
<form-field :form="form" fieldName="email" inputType="email" :label="$t('auth.forms.email')" autofocus />
|
||||
<form-field :form="form" fieldName="password" inputType="password" :label="$t('auth.forms.password')" />
|
||||
<form-buttons :isBusy="form.isBusy" :caption="$t('auth.sign_in')" />
|
||||
</form>
|
||||
<p>{{ $t('auth.forms.dont_have_account_yet') }} <router-link :to="{ name: 'register' }" class="is-link">{{ $t('auth.register') }}</router-link></p>
|
||||
<p>{{ $t('auth.forms.forgot_your_password') }} <router-link :to="{ name: 'password.request' }" class="is-link">{{ $t('auth.forms.request_password_reset') }}</router-link></p>
|
||||
<p v-if="!$root.appSettings.isDemoApp">{{ $t('auth.forms.forgot_your_password') }} <router-link :to="{ name: 'password.request' }" class="is-link">{{ $t('auth.forms.request_password_reset') }}</router-link></p>
|
||||
</form-wrapper>
|
||||
</template>
|
||||
|
||||
|
@ -2,11 +2,12 @@
|
||||
<form-wrapper :fail="fail" :success="success">
|
||||
<div class="tags has-addons">
|
||||
<span class="tag is-dark">2FAuth</span>
|
||||
<span class="tag is-info">v{{ version }}</span>
|
||||
<span class="tag is-info">v{{ $root.appVersion }}</span>
|
||||
</div>
|
||||
<form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)">
|
||||
<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>
|
||||
</form-wrapper>
|
||||
</template>
|
||||
@ -22,13 +23,13 @@
|
||||
fail: '',
|
||||
form: new Form({
|
||||
lang: this.$root.$i18n.locale,
|
||||
showTokenAsDot: Boolean(Number(appSettings.showTokenAsDot)),
|
||||
showTokenAsDot: this.$root.appSettings.showTokenAsDot,
|
||||
closeTokenOnCopy: this.$root.appSettings.closeTokenOnCopy,
|
||||
}),
|
||||
options: [
|
||||
{ text: this.$t('languages.en'), value: 'en' },
|
||||
{ text: this.$t('languages.fr'), value: 'fr' },
|
||||
],
|
||||
version: appVersion
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@ -48,7 +49,7 @@
|
||||
this.$router.go()
|
||||
}
|
||||
else {
|
||||
appSettings = response.data.settings
|
||||
this.$root.appSettings = response.data.settings
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
|
@ -45,7 +45,8 @@
|
||||
'change_your_password' => 'Change your password',
|
||||
'password_successfully_changed' => 'Password successfully changed ',
|
||||
'edit_account' => 'Edit account',
|
||||
'profile_saved' => 'Profile successfully updated!'
|
||||
'profile_saved' => 'Profile successfully updated!',
|
||||
'welcome_to_demo_app_use_those_credentials' => 'Welcome to the 2FAuth demo.<br><br>You can connect using the email address <strong>demo@2fauth.app</strong> and the password <strong>demo</demo>',
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -20,5 +20,6 @@
|
||||
'edit' => 'Edit',
|
||||
'delete' => 'Delete',
|
||||
'save' => 'Save',
|
||||
'close' => 'Close'
|
||||
'close' => 'Close',
|
||||
'demo_do_not_post_sensitive_data' => 'This is a demo app, do not post any sensitive data',
|
||||
];
|
@ -31,6 +31,10 @@
|
||||
'label' => 'Show generated tokens as dot',
|
||||
'help' => 'Replace generated token caracters with *** to ensure confidentiality. Do not affect the copy/paste feature.'
|
||||
],
|
||||
'close_token_on_copy' => [
|
||||
'label' => 'Close token after copy',
|
||||
'help' => 'Automatically close the popup showing the generated token after it has been copied'
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
|
@ -45,7 +45,8 @@
|
||||
'change_your_password' => 'Modifier votre mot de passe',
|
||||
'password_successfully_changed' => 'Mot de passe modifié avec succès',
|
||||
'edit_account' => 'Mis à jour du profil',
|
||||
'profile_saved' => 'Profil mis à jour avec succès !'
|
||||
'profile_saved' => 'Profil mis à jour avec succès !',
|
||||
'welcome_to_demo_app_use_those_credentials' => 'bienvenue sur la démo de 2FAuth.<br><br>Vous pouvez vous connecter en utilisant l\'adresse email <strong>demo@2fauth.app</strong> et le mot de passe <strong>demo</demo>',
|
||||
],
|
||||
|
||||
|
||||
|
@ -20,5 +20,6 @@
|
||||
'edit' => 'Modifier',
|
||||
'delete' => 'Supprimer',
|
||||
'save' => 'Enregistrer',
|
||||
'close' => 'Fermer'
|
||||
'close' => 'Fermer',
|
||||
'demo_do_not_post_sensitive_data' => 'Site de démonstration, ne postez aucune donnée sensible',
|
||||
];
|
@ -28,9 +28,14 @@
|
||||
'help' => 'Traduit l\'application dans la langue choisie'
|
||||
],
|
||||
'show_token_as_dot' => [
|
||||
'label' => 'Masquer les codes générés',
|
||||
'help' => 'Remplace les caractères des codes générés par des *** pour garantir leur confidentialité. N\'affecte pas la fonction copier/coller qui reste utilisable.'
|
||||
]
|
||||
'label' => 'Rendre illisibles les codes générés',
|
||||
'help' => 'Remplace les caractères des codes générés par des ●●● pour garantir leur confidentialité. N\'affecte pas la fonction de copier/coller qui reste utilisable.'
|
||||
],
|
||||
'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é.'
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
|
||||
|
@ -28,11 +28,13 @@
|
||||
|
||||
Route::post('logout', 'Auth\LoginController@logout');
|
||||
|
||||
Route::get('settings/account', 'Settings\AccountController@show');
|
||||
Route::patch('settings/account', 'Settings\AccountController@update');
|
||||
Route::patch('settings/password', 'Settings\PasswordController@update');
|
||||
Route::get('settings/options', 'Settings\OptionController@index');
|
||||
Route::post('settings/options', 'Settings\OptionController@store');
|
||||
Route::prefix('settings')->group(function () {
|
||||
Route::get('account', 'Settings\AccountController@show');
|
||||
Route::patch('account', 'Settings\AccountController@update');
|
||||
Route::patch('password', 'Settings\PasswordController@update');
|
||||
Route::get('options', 'Settings\OptionController@index');
|
||||
Route::post('options', 'Settings\OptionController@store');
|
||||
});
|
||||
|
||||
Route::delete('twofaccounts/batch', 'TwoFAccountController@batchDestroy');
|
||||
Route::apiResource('twofaccounts', 'TwoFAccountController');
|
||||
|
145
tests/Feature/ConsoleTest.php
Normal file
145
tests/Feature/ConsoleTest.php
Normal file
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\User;
|
||||
use Tests\TestCase;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
|
||||
class ConsoleTest extends TestCase
|
||||
{
|
||||
|
||||
/**
|
||||
* Test 2fauth:reset-demo console command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test2fauthResetDemowithoutDemoModeConsoleCommand()
|
||||
{
|
||||
$this->artisan('2fauth:reset-demo')
|
||||
->expectsOutput('2fauth:reset-demo can only run when isDemoApp option is On')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 2fauth:reset-demo console command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test2fauthResetDemowithConfirmConsoleCommand()
|
||||
{
|
||||
Config::set('app.options.isDemoApp', true);
|
||||
|
||||
$this->artisan('2fauth:reset-demo')
|
||||
->expectsOutput('This will reset the app in order to run a clean and fresh demo.')
|
||||
->expectsQuestion('To prevent any mistake please type the word "demo" to go on', 'demo')
|
||||
->expectsOutput('Demo app refreshed')
|
||||
->assertExitCode(0);
|
||||
|
||||
$user = User::find(1);
|
||||
|
||||
$response = $this->actingAs($user, 'api')
|
||||
->json('GET', '/api/twofaccounts/1')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'service' => 'Amazon',
|
||||
'icon' => 'amazon.png',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user, 'api')
|
||||
->json('GET', '/api/twofaccounts/2')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'service' => 'Apple',
|
||||
'icon' => 'apple.png',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user, 'api')
|
||||
->json('GET', '/api/twofaccounts/3')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'service' => 'Dropbox',
|
||||
'icon' => 'dropbox.png',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user, 'api')
|
||||
->json('GET', '/api/twofaccounts/4')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'service' => 'Facebook',
|
||||
'icon' => 'facebook.png',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user, 'api')
|
||||
->json('GET', '/api/twofaccounts/5')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'service' => 'Github',
|
||||
'icon' => 'github.png',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user, 'api')
|
||||
->json('GET', '/api/twofaccounts/6')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'service' => 'Google',
|
||||
'icon' => 'google.png',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user, 'api')
|
||||
->json('GET', '/api/twofaccounts/7')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'service' => 'Instagram',
|
||||
'icon' => 'instagram.png',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user, 'api')
|
||||
->json('GET', '/api/twofaccounts/8')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'service' => 'LinkedIn',
|
||||
'icon' => 'linkedin.png',
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user, 'api')
|
||||
->json('GET', '/api/twofaccounts/9')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'service' => 'Twitter',
|
||||
'icon' => 'twitter.png',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test 2fauth:reset-demo console command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test2fauthResetDemowithBadConfirmationConsoleCommand()
|
||||
{
|
||||
Config::set('app.options.isDemoApp', true);
|
||||
|
||||
$this->artisan('2fauth:reset-demo')
|
||||
->expectsQuestion('To prevent any mistake please type the word "demo" to go on', 'null')
|
||||
->expectsOutput('Bad confirmation word, nothing appened')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Test 2fauth:reset-demo console command.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test2fauthResetDemowithoutConfirmationConsoleCommand()
|
||||
{
|
||||
Config::set('app.options.isDemoApp', true);
|
||||
|
||||
$this->artisan('2fauth:reset-demo --no-confirm')
|
||||
->expectsOutput('Demo app refreshed')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
}
|
@ -32,14 +32,16 @@ public function testSettingsStorage()
|
||||
$response = $this->actingAs($this->user, 'api')
|
||||
->json('POST', '/api/settings/options', [
|
||||
'setting_1' => 'value_1',
|
||||
'setting_2' => 'value_2',
|
||||
'setting_2' => true,
|
||||
'setting_3' => false,
|
||||
])
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'message' => __('settings.forms.setting_saved'),
|
||||
'settings' => [
|
||||
'setting_1' => 'value_1',
|
||||
'setting_2' => 'value_2',
|
||||
'setting_2' => true,
|
||||
'setting_3' => false,
|
||||
]
|
||||
]);
|
||||
}
|
||||
@ -53,23 +55,17 @@ public function testSettingsStorage()
|
||||
public function testSettingsIndexListing()
|
||||
{
|
||||
option(['setting_1' => 'value_1']);
|
||||
option(['setting_2' => 'value_2']);
|
||||
option(['setting_2' => true]);
|
||||
option(['setting_3' => false]);
|
||||
|
||||
$response = $this->actingAs($this->user, 'api')
|
||||
->json('GET', '/api/settings/options')
|
||||
->assertStatus(200)
|
||||
->assertJson([
|
||||
'settings' => [
|
||||
[
|
||||
'id' => '1',
|
||||
'key' => 'setting_1',
|
||||
'value' => 'value_1'
|
||||
],
|
||||
[
|
||||
'id' => '2',
|
||||
'key' => 'setting_2',
|
||||
'value' => 'value_2'
|
||||
]
|
||||
'setting_1' => 'value_1',
|
||||
'setting_2' => true,
|
||||
'setting_3' => false,
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user