mirror of
https://github.com/Bubka/2FAuth.git
synced 2024-11-22 08:13:11 +01:00
Add auto-lock option
This commit is contained in:
parent
07df0cd5e0
commit
9b34159c4c
@ -9,6 +9,7 @@
|
|||||||
use Illuminate\Support\Facades\Lang;
|
use Illuminate\Support\Facades\Lang;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
|
||||||
class LoginController extends Controller
|
class LoginController extends Controller
|
||||||
@ -73,6 +74,8 @@ protected function sendLoginResponse(Request $request)
|
|||||||
$success['token'] = $this->guard()->user()->createToken('2FAuth')->accessToken;
|
$success['token'] = $this->guard()->user()->createToken('2FAuth')->accessToken;
|
||||||
$success['name'] = $this->guard()->user()->name;
|
$success['name'] = $this->guard()->user()->name;
|
||||||
|
|
||||||
|
$this->authenticated($request, $this->guard()->user());
|
||||||
|
|
||||||
return response()->json(['message' => $success], Response::HTTP_OK);
|
return response()->json(['message' => $success], Response::HTTP_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,6 +122,18 @@ protected function validateLogin(Request $request)
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The user has been authenticated.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param mixed $user
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function authenticated(Request $request, $user)
|
||||||
|
{
|
||||||
|
$user->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* log out current user
|
* log out current user
|
||||||
|
@ -41,6 +41,8 @@ class Kernel extends HttpKernel
|
|||||||
'api' => [
|
'api' => [
|
||||||
'throttle:60,1',
|
'throttle:60,1',
|
||||||
'bindings',
|
'bindings',
|
||||||
|
\App\Http\Middleware\LogoutInactiveUser::class,
|
||||||
|
\App\Http\Middleware\LogUserLastSeen::class,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
28
app/Http/Middleware/LogUserLastSeen.php
Normal file
28
app/Http/Middleware/LogUserLastSeen.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class LogUserLastSeen
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
|
||||||
|
if( Auth::guard('api')->check() ) {
|
||||||
|
Auth::guard('api')->user()->last_seen_at = Carbon::now()->format('Y-m-d H:i:s');
|
||||||
|
Auth::guard('api')->user()->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
52
app/Http/Middleware/LogoutInactiveUser.php
Normal file
52
app/Http/Middleware/LogoutInactiveUser.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use App\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use App\Classes\Options;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class LogoutInactiveUser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
|
||||||
|
// Not a logged in user
|
||||||
|
if (!Auth::guard('api')->check()) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = Auth::guard('api')->user();
|
||||||
|
|
||||||
|
$now = Carbon::now();
|
||||||
|
$last_seen = Carbon::parse($user->last_seen_at);
|
||||||
|
$inactiveFor = $now->diffInMinutes($last_seen);
|
||||||
|
|
||||||
|
// Fetch all setting values
|
||||||
|
$settings = Options::get();
|
||||||
|
|
||||||
|
// If user has been inactivity longer than the allowed inactivity period
|
||||||
|
if ($settings['kickUserAfter'] > 0 && $inactiveFor > $settings['kickUserAfter']) {
|
||||||
|
|
||||||
|
$user->last_seen_at = $now->format('Y-m-d H:i:s');
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$accessToken = Auth::user()->token();
|
||||||
|
$accessToken->revoke();
|
||||||
|
|
||||||
|
return response()->json(['message' => 'unauthorised'], Response::HTTP_UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
@ -37,7 +37,8 @@
|
|||||||
'closeTokenOnCopy' => false,
|
'closeTokenOnCopy' => false,
|
||||||
'useBasicQrcodeReader' => false,
|
'useBasicQrcodeReader' => false,
|
||||||
'displayMode' => 'list',
|
'displayMode' => 'list',
|
||||||
'showAccountsIcons' => true
|
'showAccountsIcons' => true,
|
||||||
|
'kickUserAfter' => '15'
|
||||||
],
|
],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -198,7 +199,7 @@
|
|||||||
App\Providers\AuthServiceProvider::class,
|
App\Providers\AuthServiceProvider::class,
|
||||||
// App\Providers\BroadcastServiceProvider::class,
|
// App\Providers\BroadcastServiceProvider::class,
|
||||||
App\Providers\EventServiceProvider::class,
|
App\Providers\EventServiceProvider::class,
|
||||||
App\Providers\RouteServiceProvider::class,
|
App\Providers\RouteServiceProvider::class
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class AddLastSeenToUsersTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->timestamp('last_seen_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('last_seen_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
3
resources/js/app.js
vendored
3
resources/js/app.js
vendored
@ -1,4 +1,5 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import mixins from './mixins'
|
||||||
import router from './routes'
|
import router from './routes'
|
||||||
import api from './api'
|
import api from './api'
|
||||||
import i18n from './langs/i18n'
|
import i18n from './langs/i18n'
|
||||||
@ -6,7 +7,6 @@ import FontAwesome from './packages/fontawesome'
|
|||||||
import Clipboard from './packages/clipboard'
|
import Clipboard from './packages/clipboard'
|
||||||
import QrcodeReader from './packages/qrcodeReader'
|
import QrcodeReader from './packages/qrcodeReader'
|
||||||
import Notifications from 'vue-notification'
|
import Notifications from 'vue-notification'
|
||||||
import App from './components/App'
|
|
||||||
|
|
||||||
import './components'
|
import './components'
|
||||||
|
|
||||||
@ -17,7 +17,6 @@ const app = new Vue({
|
|||||||
data: {
|
data: {
|
||||||
appSettings: window.appSettings
|
appSettings: window.appSettings
|
||||||
},
|
},
|
||||||
components: { App },
|
|
||||||
i18n,
|
i18n,
|
||||||
router,
|
router,
|
||||||
});
|
});
|
@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<kicker v-if="kickInactiveUser"></kicker>
|
||||||
<div v-if="$root.appSettings.isDemoApp" class="demo has-background-warning has-text-centered is-size-7-mobile">
|
<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') }}
|
{{ $t('commons.demo_do_not_post_sensitive_data') }}
|
||||||
</div>
|
</div>
|
||||||
@ -17,6 +18,14 @@
|
|||||||
data(){
|
data(){
|
||||||
return {
|
return {
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
|
||||||
|
kickInactiveUser: function () {
|
||||||
|
return parseInt(this.$root.appSettings.kickUserAfter) > 0 && this.$route.meta.requiresAuth
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
58
resources/js/components/Kicker.vue
Normal file
58
resources/js/components/Kicker.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Kicker',
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
events: ['click', 'mousedown', 'scroll', 'keypress', 'load'],
|
||||||
|
logoutTimer: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
this.events.forEach(function (event) {
|
||||||
|
window.addEventListener(event, this.resetTimer)
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
this.setTimer()
|
||||||
|
},
|
||||||
|
|
||||||
|
destroyed() {
|
||||||
|
|
||||||
|
this.events.forEach(function (event) {
|
||||||
|
window.removeEventListener(event, this.resetTimer)
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
clearTimeout(this.logoutTimer)
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
|
||||||
|
setTimer: function() {
|
||||||
|
|
||||||
|
this.logoutTimer = setTimeout(this.logoutUser, this.$root.appSettings.kickUserAfter * 60 * 1000)
|
||||||
|
},
|
||||||
|
|
||||||
|
logoutUser: function() {
|
||||||
|
|
||||||
|
clearTimeout(this.logoutTimer)
|
||||||
|
|
||||||
|
this.appLogout
|
||||||
|
},
|
||||||
|
|
||||||
|
resetTimer: function() {
|
||||||
|
|
||||||
|
clearTimeout(this.logoutTimer)
|
||||||
|
|
||||||
|
this.setTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
@ -162,9 +162,11 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
clipboardSuccessHandler ({ value, event }) {
|
clipboardSuccessHandler ({ value, event }) {
|
||||||
console.log('success', value)
|
|
||||||
|
|
||||||
if(this.$root.appSettings.closeTokenOnCopy) {
|
if(this.$root.appSettings.kickUserAfter == -1) {
|
||||||
|
this.appLogout()
|
||||||
|
}
|
||||||
|
else if(this.$root.appSettings.closeTokenOnCopy) {
|
||||||
this.$parent.isActive = false
|
this.$parent.isActive = false
|
||||||
this.clearOTP()
|
this.clearOTP()
|
||||||
}
|
}
|
||||||
|
22
resources/js/components/index.js
vendored
22
resources/js/components/index.js
vendored
@ -1,16 +1,19 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Button from './Button'
|
import App from './App'
|
||||||
import FieldError from './FieldError'
|
import Button from './Button'
|
||||||
import FormWrapper from './FormWrapper'
|
import FieldError from './FieldError'
|
||||||
import FormField from './FormField'
|
import FormWrapper from './FormWrapper'
|
||||||
import FormSelect from './FormSelect'
|
import FormField from './FormField'
|
||||||
import FormSwitch from './FormSwitch'
|
import FormSelect from './FormSelect'
|
||||||
|
import FormSwitch from './FormSwitch'
|
||||||
import FormCheckbox from './FormCheckbox'
|
import FormCheckbox from './FormCheckbox'
|
||||||
import FormButtons from './FormButtons'
|
import FormButtons from './FormButtons'
|
||||||
import VueFooter from './Footer'
|
import VueFooter from './Footer'
|
||||||
|
import Kicker from './Kicker'
|
||||||
|
|
||||||
// Components that are registered globaly.
|
// Components that are registered globaly.
|
||||||
[
|
[
|
||||||
|
App,
|
||||||
Button,
|
Button,
|
||||||
FieldError,
|
FieldError,
|
||||||
FormWrapper,
|
FormWrapper,
|
||||||
@ -20,6 +23,7 @@ import VueFooter from './Footer'
|
|||||||
FormCheckbox,
|
FormCheckbox,
|
||||||
FormButtons,
|
FormButtons,
|
||||||
VueFooter,
|
VueFooter,
|
||||||
|
Kicker
|
||||||
].forEach(Component => {
|
].forEach(Component => {
|
||||||
Vue.component(Component.name, Component)
|
Vue.component(Component.name, Component)
|
||||||
})
|
})
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
<template>
|
<template>
|
||||||
<form-wrapper>
|
<form-wrapper>
|
||||||
<form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)">
|
<form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)">
|
||||||
|
<h4 class="title is-4">{{ $t('settings.general') }}</h4>
|
||||||
<form-select :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
|
<form-select :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
|
||||||
<form-select :options="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
|
<form-select :options="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
|
||||||
|
<form-checkbox :form="form" fieldName="showAccountsIcons" :label="$t('settings.forms.show_accounts_icons.label')" :help="$t('settings.forms.show_accounts_icons.help')" />
|
||||||
|
<h4 class="title is-4">{{ $t('settings.security') }}</h4>
|
||||||
|
<form-select :options="kickUserAfters" :form="form" fieldName="kickUserAfter" :label="$t('settings.forms.auto_lock.label')" :help="$t('settings.forms.auto_lock.help')" />
|
||||||
<form-checkbox :form="form" fieldName="showTokenAsDot" :label="$t('settings.forms.show_token_as_dot.label')" :help="$t('settings.forms.show_token_as_dot.help')" />
|
<form-checkbox :form="form" fieldName="showTokenAsDot" :label="$t('settings.forms.show_token_as_dot.label')" :help="$t('settings.forms.show_token_as_dot.help')" />
|
||||||
<form-checkbox :form="form" fieldName="closeTokenOnCopy" :label="$t('settings.forms.close_token_on_copy.label')" :help="$t('settings.forms.close_token_on_copy.help')" />
|
<form-checkbox :form="form" fieldName="closeTokenOnCopy" :label="$t('settings.forms.close_token_on_copy.label')" :help="$t('settings.forms.close_token_on_copy.help')" />
|
||||||
|
<h4 class="title is-4">{{ $t('settings.advanced') }}</h4>
|
||||||
<form-checkbox :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
|
<form-checkbox :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
|
||||||
<form-checkbox :form="form" fieldName="showAccountsIcons" :label="$t('settings.forms.show_accounts_icons.label')" :help="$t('settings.forms.show_accounts_icons.help')" />
|
|
||||||
</form>
|
</form>
|
||||||
</form-wrapper>
|
</form-wrapper>
|
||||||
</template>
|
</template>
|
||||||
@ -25,6 +29,7 @@
|
|||||||
useBasicQrcodeReader: this.$root.appSettings.useBasicQrcodeReader,
|
useBasicQrcodeReader: this.$root.appSettings.useBasicQrcodeReader,
|
||||||
showAccountsIcons: this.$root.appSettings.showAccountsIcons,
|
showAccountsIcons: this.$root.appSettings.showAccountsIcons,
|
||||||
displayMode: this.$root.appSettings.displayMode,
|
displayMode: this.$root.appSettings.displayMode,
|
||||||
|
kickUserAfter: this.$root.appSettings.kickUserAfter,
|
||||||
}),
|
}),
|
||||||
langs: [
|
langs: [
|
||||||
{ text: this.$t('languages.en'), value: 'en' },
|
{ text: this.$t('languages.en'), value: 'en' },
|
||||||
@ -33,6 +38,17 @@
|
|||||||
layouts: [
|
layouts: [
|
||||||
{ text: this.$t('settings.forms.grid'), value: 'grid' },
|
{ text: this.$t('settings.forms.grid'), value: 'grid' },
|
||||||
{ text: this.$t('settings.forms.list'), value: 'list' },
|
{ text: this.$t('settings.forms.list'), value: 'list' },
|
||||||
|
],
|
||||||
|
kickUserAfters: [
|
||||||
|
{ text: this.$t('settings.forms.never'), value: '0' },
|
||||||
|
{ text: this.$t('settings.forms.on_token_copy'), value: '-1' },
|
||||||
|
{ text: this.$t('settings.forms.1_minutes'), value: '1' },
|
||||||
|
{ text: this.$t('settings.forms.5_minutes'), value: '5' },
|
||||||
|
{ text: this.$t('settings.forms.10_minutes'), value: '10' },
|
||||||
|
{ text: this.$t('settings.forms.15_minutes'), value: '15' },
|
||||||
|
{ text: this.$t('settings.forms.30_minutes'), value: '30' },
|
||||||
|
{ text: this.$t('settings.forms.1_hour'), value: '60' },
|
||||||
|
{ text: this.$t('settings.forms.1_day'), value: '1440' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -20,6 +20,9 @@
|
|||||||
'confirm' => [
|
'confirm' => [
|
||||||
|
|
||||||
],
|
],
|
||||||
|
'general' => 'General',
|
||||||
|
'security' => 'Security',
|
||||||
|
'advanced' => 'Advanced',
|
||||||
'forms' => [
|
'forms' => [
|
||||||
'edit_settings' => 'Edit settings',
|
'edit_settings' => 'Edit settings',
|
||||||
'setting_saved' => 'Settings saved',
|
'setting_saved' => 'Settings saved',
|
||||||
@ -49,6 +52,19 @@
|
|||||||
'label' => 'Show icons',
|
'label' => 'Show icons',
|
||||||
'help' => 'Show icons accounts in the main view'
|
'help' => 'Show icons accounts in the main view'
|
||||||
],
|
],
|
||||||
|
'auto_lock' => [
|
||||||
|
'label' => 'Auto lock',
|
||||||
|
'help' => 'Log out the user automatically in case of inactivity'
|
||||||
|
],
|
||||||
|
'never' => 'Never',
|
||||||
|
'on_token_copy' => 'On security code copy',
|
||||||
|
'1_minutes' => 'After 1 minute',
|
||||||
|
'5_minutes' => 'After 5 minutes',
|
||||||
|
'10_minutes' => 'After 10 minutes',
|
||||||
|
'15_minutes' => 'After 15 minutes',
|
||||||
|
'30_minutes' => 'After 15 minutes',
|
||||||
|
'1_hour' => 'After 1 hour',
|
||||||
|
'1_day' => 'After 1 day',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,9 @@
|
|||||||
'confirm' => [
|
'confirm' => [
|
||||||
|
|
||||||
],
|
],
|
||||||
|
'general' => 'General',
|
||||||
|
'security' => 'Sécurité',
|
||||||
|
'advanced' => 'Avancés',
|
||||||
'forms' => [
|
'forms' => [
|
||||||
'edit_settings' => 'Modifier les réglages',
|
'edit_settings' => 'Modifier les réglages',
|
||||||
'setting_saved' => 'Réglages sauvegardés',
|
'setting_saved' => 'Réglages sauvegardés',
|
||||||
@ -49,7 +52,19 @@
|
|||||||
'label' => 'Afficher les icônes',
|
'label' => 'Afficher les icônes',
|
||||||
'help' => 'Affiche les icônes des comptes dans la vue principale'
|
'help' => 'Affiche les icônes des comptes dans la vue principale'
|
||||||
],
|
],
|
||||||
|
'auto_lock' => [
|
||||||
|
'label' => 'Verouillage automatique',
|
||||||
|
'help' => 'Déconnecter automatiquement l\'utilisateur en cas d\'inactivité'
|
||||||
|
],
|
||||||
|
'never' => 'Jamais',
|
||||||
|
'on_token_copy' => 'Après copie d\'un code de sécurité',
|
||||||
|
'1_minutes' => 'Après 1 minute',
|
||||||
|
'5_minutes' => 'Après 5 minutes',
|
||||||
|
'10_minutes' => 'Après 10 minutes',
|
||||||
|
'15_minutes' => 'Après 15 minutes',
|
||||||
|
'30_minutes' => 'Après 15 minutes',
|
||||||
|
'1_hour' => 'Après 1 heure',
|
||||||
|
'1_day' => 'Après 1 journée',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user