Merge branch 'release/1.1.0'

This commit is contained in:
Bubka 2020-03-23 23:13:36 +01:00
commit 46cd2049aa
34 changed files with 596 additions and 105 deletions

View File

@ -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
View 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]);
}
}
}

File diff suppressed because one or more lines are too long

View File

@ -41,7 +41,9 @@ public function update(Request $request)
return response()->json(['message' => __('errors.wrong_current_password')], 400);
}
if (!config('app.options.isDemoApp') ) {
tap($user)->update($request->only('name', 'email'));
}
return response()->json([
'message' => __('auth.forms.profile_saved'),

View File

@ -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);
}
}

View File

@ -27,9 +27,11 @@ public function update(Request $request)
return response()->json(['message' => __('errors.wrong_current_password')], 400);
}
if (!config('app.options.isDemoApp') ) {
$request->user()->update([
'password' => bcrypt($request->password),
]);
}
return response()->json(['message' => __('auth.forms.password_successfully_changed')]);
}

View File

@ -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
View 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

View File

@ -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,
],
/*
|--------------------------------------------------------------------------

View 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
View File

@ -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",

View File

@ -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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

1
public/js/manifest.js vendored Normal file
View 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

File diff suppressed because one or more lines are too long

7
public/mix-manifest.json Normal file
View 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
View File

@ -10,6 +10,10 @@ import './components'
const app = new Vue({
el: '#app',
data: {
appSettings: window.appSettings,
appVersion: window.appVersion
},
components: { App },
i18n,
router,

View File

@ -1,7 +1,12 @@
<template>
<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>

View File

@ -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>

View File

@ -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 }) {

View File

@ -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é."
}
}
},

View File

@ -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') }}&nbsp;<router-link :to="{ name: 'register' }" class="is-link">{{ $t('auth.register') }}</router-link></p>
<p>{{ $t('auth.forms.forgot_your_password') }}&nbsp;<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') }}&nbsp;<router-link :to="{ name: 'password.request' }" class="is-link">{{ $t('auth.forms.request_password_reset') }}</router-link></p>
</form-wrapper>
</template>

View File

@ -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 => {

View File

@ -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>',
],
];

View File

@ -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',
];

View File

@ -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'
],
],

View File

@ -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>',
],

View File

@ -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',
];

View File

@ -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é.'
],
],

View File

@ -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');

View 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);
}
}

View File

@ -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,
]
]);
}