mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-06-20 03:38:06 +02:00
Add Admin role & split settings between appSettings and userPreferences
This commit is contained in:
parent
d0401ced5d
commit
5e5e50d053
@ -98,8 +98,8 @@ class SettingController extends Controller
|
|||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$optionsConfig = config('2fauth.options');
|
$appSettings = config('2fauth.settings');
|
||||||
if (array_key_exists($settingName, $optionsConfig)) {
|
if (array_key_exists($settingName, $appSettings)) {
|
||||||
return response()->json(
|
return response()->json(
|
||||||
['message' => 'bad request',
|
['message' => 'bad request',
|
||||||
'reason' => [__('errors.delete_user_setting_only')],
|
'reason' => [__('errors.delete_user_setting_only')],
|
||||||
|
@ -3,9 +3,11 @@
|
|||||||
namespace App\Api\v1\Controllers;
|
namespace App\Api\v1\Controllers;
|
||||||
|
|
||||||
use App\Api\v1\Resources\UserResource;
|
use App\Api\v1\Resources\UserResource;
|
||||||
|
use App\Api\v1\Requests\SettingUpdateRequest;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
@ -25,4 +27,67 @@ class UserController extends Controller
|
|||||||
? new UserResource($user)
|
? new UserResource($user)
|
||||||
: response()->json(['name' => null], 200);
|
: response()->json(['name' => null], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all preferences
|
||||||
|
*
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function allPreferences(Request $request)
|
||||||
|
{
|
||||||
|
$preferences = $request->user()->preferences;
|
||||||
|
$jsonPrefs = collect([]);
|
||||||
|
|
||||||
|
$preferences->each(function (mixed $item, string $key) use ($jsonPrefs) {
|
||||||
|
$jsonPrefs->push([
|
||||||
|
'key' => $key,
|
||||||
|
'value' => $item,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return response()->json($jsonPrefs->all(), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a preference
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param string $preferenceName
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function showPreference(Request $request, string $preferenceName)
|
||||||
|
{
|
||||||
|
if (! Arr::exists($request->user()->preferences, $preferenceName)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'key' => $preferenceName,
|
||||||
|
'value' => $request->user()->preferences[$preferenceName],
|
||||||
|
], 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save a preference
|
||||||
|
*
|
||||||
|
* @param \App\Api\v1\Requests\SettingUpdateRequest $request
|
||||||
|
* @param string $preferenceName
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function setPreference(SettingUpdateRequest $request, string $preferenceName)
|
||||||
|
{
|
||||||
|
if (! Arr::exists($request->user()->preferences, $preferenceName)) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$request->user()['preferences->'.$preferenceName] = $validated['value'];
|
||||||
|
$request->user()->save();
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'key' => $preferenceName,
|
||||||
|
'value' => $request->user()->preferences[$preferenceName],
|
||||||
|
], 201);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,9 +20,10 @@ class UserResource extends JsonResource
|
|||||||
public function toArray($request)
|
public function toArray($request)
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $this->when(! is_null($request->user()), $this->id),
|
'id' => $this->when(! is_null($request->user()), $this->id),
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'email' => $this->when(! is_null($request->user()), $this->email),
|
'email' => $this->when(! is_null($request->user()), $this->email),
|
||||||
|
'is_admin' => $this->when(! is_null($request->user()), $this->is_admin),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,12 @@ class Handler extends ExceptionHandler
|
|||||||
], 404);
|
], 404);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$this->renderable(function (\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException $exception, $request) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'unauthorized',
|
||||||
|
], 403);
|
||||||
|
});
|
||||||
|
|
||||||
$this->renderable(function (InvalidOtpParameterException $exception, $request) {
|
$this->renderable(function (InvalidOtpParameterException $exception, $request) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'message' => 'invalid OTP parameters',
|
'message' => 'invalid OTP parameters',
|
||||||
|
@ -4,6 +4,7 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
use App\Events\ScanForNewReleaseCalled;
|
use App\Events\ScanForNewReleaseCalled;
|
||||||
use App\Facades\Settings;
|
use App\Facades\Settings;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
|
|
||||||
class SinglePageController extends Controller
|
class SinglePageController extends Controller
|
||||||
@ -20,18 +21,17 @@ class SinglePageController extends Controller
|
|||||||
$subdir = config('2fauth.config.appSubdirectory') ? '/' . config('2fauth.config.appSubdirectory') : '';
|
$subdir = config('2fauth.config.appSubdirectory') ? '/' . config('2fauth.config.appSubdirectory') : '';
|
||||||
|
|
||||||
return view('landing')->with([
|
return view('landing')->with([
|
||||||
'theme' => Settings::get('theme'),
|
|
||||||
'appSettings' => Settings::all()->toJson(),
|
'appSettings' => Settings::all()->toJson(),
|
||||||
'appConfig' => collect([
|
'appConfig' => collect([
|
||||||
'proxyAuth' => config('auth.defaults.guard') === 'reverse-proxy-guard' ? true : false,
|
'proxyAuth' => config('auth.defaults.guard') === 'reverse-proxy-guard' ? true : false,
|
||||||
'proxyLogoutUrl' => config('2fauth.config.proxyLogoutUrl') ? config('2fauth.config.proxyLogoutUrl') : false,
|
'proxyLogoutUrl' => config('2fauth.config.proxyLogoutUrl') ? config('2fauth.config.proxyLogoutUrl') : false,
|
||||||
'subdirectory' => $subdir,
|
'subdirectory' => $subdir,
|
||||||
])->toJson(),
|
])->toJson(),
|
||||||
'subdirectory' => $subdir,
|
'userPreferences' => Auth::user()->preferences ?? collect(config('2fauth.preferences')),
|
||||||
'lang' => App::currentLocale(),
|
'subdirectory' => $subdir,
|
||||||
'isDemoApp' => config('2fauth.config.isDemoApp') ? 'true' : 'false',
|
'isDemoApp' => config('2fauth.config.isDemoApp') ? 'true' : 'false',
|
||||||
'isTestingApp' => config('2fauth.config.isTestingApp') ? 'true' : 'false',
|
'isTestingApp' => config('2fauth.config.isTestingApp') ? 'true' : 'false',
|
||||||
'locales' => collect(config('2fauth.locales'))->toJson(), /** @phpstan-ignore-line */
|
'locales' => collect(config('2fauth.locales'))->toJson(), /** @phpstan-ignore-line */
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,7 @@ class Kernel extends HttpKernel
|
|||||||
*/
|
*/
|
||||||
protected $routeMiddleware = [
|
protected $routeMiddleware = [
|
||||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||||
|
'admin' => \App\Http\Middleware\AdminOnly::class,
|
||||||
'guest' => \App\Http\Middleware\RejectIfAuthenticated::class,
|
'guest' => \App\Http\Middleware\RejectIfAuthenticated::class,
|
||||||
'SkipIfAuthenticated' => \App\Http\Middleware\SkipIfAuthenticated::class,
|
'SkipIfAuthenticated' => \App\Http\Middleware\SkipIfAuthenticated::class,
|
||||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||||
|
26
app/Http/Middleware/AdminOnly.php
Normal file
26
app/Http/Middleware/AdminOnly.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
|
||||||
|
class AdminOnly
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
if (! Auth::user()->is_admin) {
|
||||||
|
throw new AuthorizationException;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||||
|
|
||||||
class Authenticate extends Middleware
|
class Authenticate extends Middleware
|
||||||
@ -33,6 +34,14 @@ class Authenticate extends Middleware
|
|||||||
if ($this->auth->guard($guard)->check()) {
|
if ($this->auth->guard($guard)->check()) {
|
||||||
$this->auth->shouldUse($guard);
|
$this->auth->shouldUse($guard);
|
||||||
|
|
||||||
|
// We now have an authenticated user so we override the locale already set
|
||||||
|
// by the SetLanguage global middleware
|
||||||
|
$lang = $this->auth->guard()->user()->preferences['lang'] ?? null;
|
||||||
|
|
||||||
|
if ($lang && in_array($lang, config('2fauth.locales')) && !App::isLocale($lang)) {
|
||||||
|
App::setLocale($lang);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ class KickOutInactiveUser
|
|||||||
$inactiveFor = $now->diffInSeconds(Carbon::parse($user->last_seen_at));
|
$inactiveFor = $now->diffInSeconds(Carbon::parse($user->last_seen_at));
|
||||||
|
|
||||||
// Fetch all setting values
|
// Fetch all setting values
|
||||||
$kickUserAfterXSecond = intval(Settings::get('kickUserAfter')) * 60;
|
$kickUserAfterXSecond = intval($user->preferences['kickUserAfter']) * 60;
|
||||||
|
|
||||||
// If user has been inactive longer than the allowed inactivity period
|
// If user has been inactive longer than the allowed inactivity period
|
||||||
if ($kickUserAfterXSecond > 0 && $inactiveFor > $kickUserAfterXSecond) {
|
if ($kickUserAfterXSecond > 0 && $inactiveFor > $kickUserAfterXSecond) {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use App\Facades\Settings;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Support\Facades\App;
|
use Illuminate\Support\Facades\App;
|
||||||
|
|
||||||
@ -17,34 +17,42 @@ class SetLanguage
|
|||||||
*/
|
*/
|
||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
// 3 possible cases here:
|
// 2 possible cases here:
|
||||||
// - The user has choosen a specific language among those available in the Setting view of 2FAuth
|
// - The http client send an accept-language header
|
||||||
// - The client send an accept-language header
|
// - No language is specified
|
||||||
// - No language is passed from the client
|
|
||||||
//
|
//
|
||||||
// We prioritize the user defined one, then the request header one, and finally the fallback one.
|
// We honor the language requested in the header or we use the fallback one.
|
||||||
// FI: Settings::get() always returns a fallback value
|
// Note that if a user is authenticated later by the auth guard, the app locale
|
||||||
$lang = Settings::get('lang');
|
// will be overriden if the user has set a specific language in its preferences.
|
||||||
|
|
||||||
if ($lang === 'browser') {
|
$lang = config('app.fallback_locale');
|
||||||
$lang = config('app.fallback_locale');
|
$accepted = str_replace(' ', '', $request->header('Accept-Language'));
|
||||||
$accepted = str_replace(' ', '', $request->header('Accept-Language'));
|
|
||||||
|
|
||||||
if ($accepted && $accepted !== '*') {
|
if ($accepted && $accepted !== '*') {
|
||||||
$prefLocales = array_reduce(
|
$prefLocales = array_reduce(
|
||||||
array_diff(explode(',', $accepted), ['*']),
|
array_diff(explode(',', $accepted), ['*']),
|
||||||
function ($res, $el) {
|
function ($langs, $langItem) {
|
||||||
[$l, $q] = array_merge(explode(';q=', $el), [1]);
|
[$langLong, $weight] = array_merge(explode(';q=', $langItem), [1]);
|
||||||
$res[$l] = (float) $q;
|
$langShort = substr($langLong, 0, 2);
|
||||||
|
if (array_key_exists($langShort, $langs)) {
|
||||||
|
if ($langs[$langShort] < $weight) {
|
||||||
|
$langs[$langShort] = (float) $weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else $langs[$langShort] = (float) $weight;
|
||||||
|
|
||||||
return $res;
|
return $langs;
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
arsort($prefLocales);
|
arsort($prefLocales);
|
||||||
|
|
||||||
// We only keep the primary language passed via the header.
|
// We take the first accepted language available
|
||||||
$lang = array_key_first($prefLocales);
|
foreach ($prefLocales as $locale => $weight) {
|
||||||
|
if (in_array($locale, config('2fauth.locales'))) {
|
||||||
|
$lang = $locale;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ use Illuminate\Notifications\Notifiable;
|
|||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Laragear\WebAuthn\WebAuthnAuthentication;
|
use Laragear\WebAuthn\WebAuthnAuthentication;
|
||||||
use Laravel\Passport\HasApiTokens;
|
use Laravel\Passport\HasApiTokens;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
class User extends Authenticatable implements WebAuthnAuthenticatable
|
class User extends Authenticatable implements WebAuthnAuthenticatable
|
||||||
{
|
{
|
||||||
@ -42,6 +43,7 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
|
|||||||
*/
|
*/
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'email_verified_at' => 'datetime',
|
'email_verified_at' => 'datetime',
|
||||||
|
'is_admin' => 'boolean',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -57,6 +59,19 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
|
|||||||
Log::info('Password reset token sent');
|
Log::info('Password reset token sent');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Preferences attribute
|
||||||
|
*
|
||||||
|
* @param string $value
|
||||||
|
* @return \Illuminate\Support\Collection<array-key, mixed>
|
||||||
|
*/
|
||||||
|
public function getPreferencesAttribute($value)
|
||||||
|
{
|
||||||
|
$preferences = collect(config('2fauth.preferences'))->merge(json_decode($value)); /** @phpstan-ignore-line */
|
||||||
|
|
||||||
|
return $preferences;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set Email attribute
|
* set Email attribute
|
||||||
*
|
*
|
||||||
|
@ -16,7 +16,7 @@ use Throwable;
|
|||||||
class SettingService
|
class SettingService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* All user settings
|
* All settings
|
||||||
*
|
*
|
||||||
* @var Collection<string, mixed>
|
* @var Collection<string, mixed>
|
||||||
*/
|
*/
|
||||||
@ -32,7 +32,7 @@ class SettingService
|
|||||||
/**
|
/**
|
||||||
* Name of the cache item where options are persisted
|
* Name of the cache item where options are persisted
|
||||||
*/
|
*/
|
||||||
public const CACHE_ITEM_NAME = 'userOptions';
|
public const CACHE_ITEM_NAME = 'adminOptions';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@ -106,12 +106,12 @@ class SettingService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine if the given setting has been customized by the user
|
* Determine if the given setting has been edited
|
||||||
*
|
*
|
||||||
* @param string $key
|
* @param string $key
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isUserDefined($key) : bool
|
public function isEdited($key) : bool
|
||||||
{
|
{
|
||||||
return DB::table('options')->where('key', $key)->exists();
|
return DB::table('options')->where('key', $key)->exists();
|
||||||
}
|
}
|
||||||
@ -123,17 +123,14 @@ class SettingService
|
|||||||
*/
|
*/
|
||||||
private function build()
|
private function build()
|
||||||
{
|
{
|
||||||
// Get a collection of user saved options
|
// Get a collection of saved options
|
||||||
$userOptions = DB::table('options')->pluck('value', 'key');
|
$options = DB::table('options')->pluck('value', 'key');
|
||||||
$userOptions->transform(function ($item, $key) {
|
$options->transform(function ($item, $key) {
|
||||||
return $this->restoreType($item);
|
return $this->restoreType($item);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Merge 2fauth/app config values as fallback values
|
// Merge customized values with app default values
|
||||||
$settings = collect(config('2fauth.options'))->merge($userOptions); /** @phpstan-ignore-line */
|
$settings = collect(config('2fauth.settings'))->merge($options); /** @phpstan-ignore-line */
|
||||||
if (! Arr::has($settings, 'lang')) {
|
|
||||||
$settings['lang'] = 'browser';
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->settings = $settings;
|
$this->settings = $settings;
|
||||||
}
|
}
|
||||||
|
@ -46,34 +46,47 @@ return [
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Application fallback for user options
|
| Default values for app (global) settings
|
||||||
|
| These settings can be overloaded and persisted using the SettingService
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'options' => [
|
'settings' => [
|
||||||
|
'useEncryption' => false,
|
||||||
|
'checkForUpdate' => true,
|
||||||
|
'lastRadarScan' => 0,
|
||||||
|
'latestRelease' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default values for user preferences
|
||||||
|
| These settings can be overloaded and persisted by each user
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'preferences' => [
|
||||||
'showTokenAsDot' => false,
|
'showTokenAsDot' => false,
|
||||||
'closeOtpOnCopy' => false,
|
'closeOtpOnCopy' => false,
|
||||||
'copyOtpOnDisplay' => false,
|
'copyOtpOnDisplay' => false,
|
||||||
'useBasicQrcodeReader' => false,
|
'useBasicQrcodeReader' => false,
|
||||||
'displayMode' => 'list',
|
'displayMode' => 'list',
|
||||||
'showAccountsIcons' => true,
|
'showAccountsIcons' => true,
|
||||||
'kickUserAfter' => '15',
|
'kickUserAfter' => 15,
|
||||||
'activeGroup' => 0,
|
'activeGroup' => 0,
|
||||||
'rememberActiveGroup' => true,
|
'rememberActiveGroup' => true,
|
||||||
'defaultGroup' => 0,
|
'defaultGroup' => 0,
|
||||||
'useEncryption' => false,
|
|
||||||
'defaultCaptureMode' => 'livescan',
|
'defaultCaptureMode' => 'livescan',
|
||||||
'useDirectCapture' => false,
|
'useDirectCapture' => false,
|
||||||
'useWebauthnAsDefault' => false,
|
'useWebauthnAsDefault' => false,
|
||||||
'useWebauthnOnly' => false,
|
'useWebauthnOnly' => false,
|
||||||
'getOfficialIcons' => true,
|
'getOfficialIcons' => true,
|
||||||
'checkForUpdate' => true,
|
'theme' => 'system',
|
||||||
'lastRadarScan' => 0,
|
|
||||||
'latestRelease' => false,
|
|
||||||
'theme' => 'dark',
|
|
||||||
'formatPassword' => true,
|
'formatPassword' => true,
|
||||||
'formatPasswordBy' => 0.5,
|
'formatPasswordBy' => 0.5,
|
||||||
|
'lang' => 'browser',
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
@ -20,6 +20,7 @@ class DemoSeeder extends Seeder
|
|||||||
'name' => 'demo',
|
'name' => 'demo',
|
||||||
'email' => 'demo@2fauth.app',
|
'email' => 'demo@2fauth.app',
|
||||||
'password' => bcrypt('demo'),
|
'password' => bcrypt('demo'),
|
||||||
|
'is_admin' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$groupSocialNetwork = Group::create([
|
$groupSocialNetwork = Group::create([
|
||||||
|
@ -20,6 +20,7 @@ class TestingSeeder extends Seeder
|
|||||||
'name' => 'Tester',
|
'name' => 'Tester',
|
||||||
'email' => 'testing@2fauth.app',
|
'email' => 'testing@2fauth.app',
|
||||||
'password' => bcrypt('password'),
|
'password' => bcrypt('password'),
|
||||||
|
'is_admin' => 1,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$groupSocialNetwork = Group::create([
|
$groupSocialNetwork = Group::create([
|
||||||
|
@ -18,6 +18,7 @@ class UsersTableSeeder extends Seeder
|
|||||||
'name' => 'admin',
|
'name' => 'admin',
|
||||||
'email' => 'admin@example.org',
|
'email' => 'admin@example.org',
|
||||||
'password' => bcrypt('password'),
|
'password' => bcrypt('password'),
|
||||||
|
'is_admin' => 1,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
5
resources/js/app.js
vendored
5
resources/js/app.js
vendored
@ -17,6 +17,7 @@ const app = new Vue({
|
|||||||
data: {
|
data: {
|
||||||
appSettings: window.appSettings,
|
appSettings: window.appSettings,
|
||||||
appConfig: window.appConfig,
|
appConfig: window.appConfig,
|
||||||
|
userPreferences: window.userPreferences,
|
||||||
isDemoApp: window.isDemoApp,
|
isDemoApp: window.isDemoApp,
|
||||||
isTestingApp: window.isTestingApp,
|
isTestingApp: window.isTestingApp,
|
||||||
prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)').matches
|
prefersDarkScheme: window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
@ -24,8 +25,8 @@ const app = new Vue({
|
|||||||
|
|
||||||
computed: {
|
computed: {
|
||||||
showDarkMode: function() {
|
showDarkMode: function() {
|
||||||
return this.appSettings.theme == 'dark' ||
|
return this.userPreferences.theme == 'dark' ||
|
||||||
(this.appSettings.theme == 'system' && this.prefersDarkScheme)
|
(this.userPreferences.theme == 'system' && this.prefersDarkScheme)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
computed: {
|
computed: {
|
||||||
|
|
||||||
kickInactiveUser: function () {
|
kickInactiveUser: function () {
|
||||||
return parseInt(this.$root.appSettings.kickUserAfter) > 0 && this.$route.meta.requiresAuth
|
return parseInt(this.$root.userPreferences.kickUserAfter) > 0 && this.$route.meta.requiresAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
setTimer: function() {
|
setTimer: function() {
|
||||||
|
|
||||||
this.logoutTimer = setTimeout(this.logoutUser, this.$root.appSettings.kickUserAfter * 60 * 1000)
|
this.logoutTimer = setTimeout(this.logoutUser, this.$root.userPreferences.kickUserAfter * 60 * 1000)
|
||||||
},
|
},
|
||||||
|
|
||||||
logoutUser: function() {
|
logoutUser: function() {
|
||||||
|
@ -63,14 +63,14 @@
|
|||||||
|
|
||||||
displayedOtp() {
|
displayedOtp() {
|
||||||
let pwd = this.internal_password
|
let pwd = this.internal_password
|
||||||
if (this.$root.appSettings.formatPassword && pwd.length > 0) {
|
if (this.$root.userPreferences.formatPassword && pwd.length > 0) {
|
||||||
const x = Math.ceil(this.$root.appSettings.formatPasswordBy < 1 ? pwd.length * this.$root.appSettings.formatPasswordBy : this.$root.appSettings.formatPasswordBy)
|
const x = Math.ceil(this.$root.userPreferences.formatPasswordBy < 1 ? pwd.length * this.$root.userPreferences.formatPasswordBy : this.$root.userPreferences.formatPasswordBy)
|
||||||
const chunks = pwd.match(new RegExp(`.{1,${x}}`, 'g'));
|
const chunks = pwd.match(new RegExp(`.{1,${x}}`, 'g'));
|
||||||
if (chunks) {
|
if (chunks) {
|
||||||
pwd = chunks.join(' ')
|
pwd = chunks.join(' ')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.$root.appSettings.showOtpAsDot ? pwd.replace(/[0-9]/g, '●') : pwd
|
return this.$root.userPreferences.showOtpAsDot ? pwd.replace(/[0-9]/g, '●') : pwd
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -94,10 +94,10 @@
|
|||||||
const success = this.$clipboard(rawOTP)
|
const success = this.$clipboard(rawOTP)
|
||||||
|
|
||||||
if (success == true) {
|
if (success == true) {
|
||||||
if(this.$root.appSettings.kickUserAfter == -1) {
|
if(this.$root.userPreferences.kickUserAfter == -1) {
|
||||||
this.appLogout()
|
this.appLogout()
|
||||||
}
|
}
|
||||||
else if(this.$root.appSettings.closeOtpOnCopy) {
|
else if(this.$root.userPreferences.closeOtpOnCopy) {
|
||||||
this.$parent.isActive = false
|
this.$parent.isActive = false
|
||||||
this.clearOTP()
|
this.clearOTP()
|
||||||
}
|
}
|
||||||
@ -214,7 +214,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.axios(request).then(response => {
|
await this.axios(request).then(response => {
|
||||||
if(this.$root.appSettings.copyOtpOnDisplay) {
|
if(this.$root.userPreferences.copyOtpOnDisplay) {
|
||||||
this.copyOTP(response.data.password)
|
this.copyOTP(response.data.password)
|
||||||
}
|
}
|
||||||
password = response.data
|
password = response.data
|
||||||
|
@ -130,8 +130,8 @@
|
|||||||
loadingLabel: 'refreshing'
|
loadingLabel: 'refreshing'
|
||||||
}" > -->
|
}" > -->
|
||||||
<draggable v-model="filteredAccounts" @start="drag = true" @end="saveOrder" ghost-class="ghost" handle=".tfa-dots" animation="200" class="accounts">
|
<draggable v-model="filteredAccounts" @start="drag = true" @end="saveOrder" ghost-class="ghost" handle=".tfa-dots" animation="200" class="accounts">
|
||||||
<transition-group class="columns is-multiline" :class="{ 'is-centered': $root.appSettings.displayMode === 'grid' }" type="transition" :name="!drag ? 'flip-list' : null">
|
<transition-group class="columns is-multiline" :class="{ 'is-centered': $root.userPreferences.displayMode === 'grid' }" type="transition" :name="!drag ? 'flip-list' : null">
|
||||||
<div :class="[$root.appSettings.displayMode === 'grid' ? 'tfa-grid' : 'tfa-list']" class="column is-narrow" v-for="account in filteredAccounts" :key="account.id">
|
<div :class="[$root.userPreferences.displayMode === 'grid' ? 'tfa-grid' : 'tfa-list']" class="column is-narrow" v-for="account in filteredAccounts" :key="account.id">
|
||||||
<div class="tfa-container">
|
<div class="tfa-container">
|
||||||
<transition name="slideCheckbox">
|
<transition name="slideCheckbox">
|
||||||
<div class="tfa-cell tfa-checkbox" v-if="editMode">
|
<div class="tfa-cell tfa-checkbox" v-if="editMode">
|
||||||
@ -143,7 +143,7 @@
|
|||||||
</transition>
|
</transition>
|
||||||
<div tabindex="0" class="tfa-cell tfa-content is-size-3 is-size-4-mobile" @click="showAccount(account)" @keyup.enter="showAccount(account)" role="button">
|
<div tabindex="0" class="tfa-cell tfa-content is-size-3 is-size-4-mobile" @click="showAccount(account)" @keyup.enter="showAccount(account)" role="button">
|
||||||
<div class="tfa-text has-ellipsis">
|
<div class="tfa-text has-ellipsis">
|
||||||
<img :src="$root.appConfig.subdirectory + '/storage/icons/' + account.icon" v-if="account.icon && $root.appSettings.showAccountsIcons" :alt="$t('twofaccounts.icon_for_account_x_at_service_y', {account: account.account, service: account.service})">
|
<img :src="$root.appConfig.subdirectory + '/storage/icons/' + account.icon" v-if="account.icon && $root.userPreferences.showAccountsIcons" :alt="$t('twofaccounts.icon_for_account_x_at_service_y', {account: account.account, service: account.service})">
|
||||||
{{ displayService(account.service) }}<font-awesome-icon class="has-text-danger is-size-5 ml-2" v-if="$root.appSettings.useEncryption && account.account === $t('errors.indecipherable')" :icon="['fas', 'exclamation-circle']" />
|
{{ displayService(account.service) }}<font-awesome-icon class="has-text-danger is-size-5 ml-2" v-if="$root.appSettings.useEncryption && account.account === $t('errors.indecipherable')" :icon="['fas', 'exclamation-circle']" />
|
||||||
<span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey ">{{ account.account }}</span>
|
<span class="is-family-primary is-size-6 is-size-7-mobile has-text-grey ">{{ account.account }}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -245,8 +245,8 @@
|
|||||||
* ~ The Edit mode
|
* ~ The Edit mode
|
||||||
* - User are automatically pushed to the start view if there is no account to list.
|
* - User are automatically pushed to the start view if there is no account to list.
|
||||||
* - The view is affected by :
|
* - The view is affected by :
|
||||||
* ~ 'appSettings.showAccountsIcons' toggle the icon visibility
|
* ~ 'userPreferences.showAccountsIcons' toggle the icon visibility
|
||||||
* ~ 'appSettings.displayMode' change the account appearance
|
* ~ 'userPreferences.displayMode' change the account appearance
|
||||||
*
|
*
|
||||||
* Input :
|
* Input :
|
||||||
* - The 'initialEditMode' props : allows to load the view directly in Edit mode
|
* - The 'initialEditMode' props : allows to load the view directly in Edit mode
|
||||||
@ -274,7 +274,7 @@
|
|||||||
showGroupSelector: false,
|
showGroupSelector: false,
|
||||||
moveAccountsTo: false,
|
moveAccountsTo: false,
|
||||||
form: new Form({
|
form: new Form({
|
||||||
value: this.$root.appSettings.activeGroup,
|
value: this.$root.userPreferences.activeGroup,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -288,10 +288,10 @@
|
|||||||
|
|
||||||
return this.accounts.filter(
|
return this.accounts.filter(
|
||||||
item => {
|
item => {
|
||||||
if( parseInt(this.$root.appSettings.activeGroup) > 0 ) {
|
if( parseInt(this.$root.userPreferences.activeGroup) > 0 ) {
|
||||||
return ((item.service ? item.service.toLowerCase().includes(this.search.toLowerCase()) : false) ||
|
return ((item.service ? item.service.toLowerCase().includes(this.search.toLowerCase()) : false) ||
|
||||||
item.account.toLowerCase().includes(this.search.toLowerCase())) &&
|
item.account.toLowerCase().includes(this.search.toLowerCase())) &&
|
||||||
(item.group_id == parseInt(this.$root.appSettings.activeGroup))
|
(item.group_id == parseInt(this.$root.userPreferences.activeGroup))
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return ((item.service ? item.service.toLowerCase().includes(this.search.toLowerCase()) : false) ||
|
return ((item.service ? item.service.toLowerCase().includes(this.search.toLowerCase()) : false) ||
|
||||||
@ -316,7 +316,7 @@
|
|||||||
* Returns the name of a group
|
* Returns the name of a group
|
||||||
*/
|
*/
|
||||||
activeGroupName() {
|
activeGroupName() {
|
||||||
let g = this.groups.find(el => el.id === parseInt(this.$root.appSettings.activeGroup))
|
let g = this.groups.find(el => el.id === parseInt(this.$root.userPreferences.activeGroup))
|
||||||
|
|
||||||
if(g) {
|
if(g) {
|
||||||
return g.name
|
return g.name
|
||||||
@ -369,10 +369,10 @@
|
|||||||
* Route user to the appropriate submitting view
|
* Route user to the appropriate submitting view
|
||||||
*/
|
*/
|
||||||
start() {
|
start() {
|
||||||
if( this.$root.appSettings.useDirectCapture && this.$root.appSettings.defaultCaptureMode === 'advancedForm' ) {
|
if( this.$root.userPreferences.useDirectCapture && this.$root.userPreferences.defaultCaptureMode === 'advancedForm' ) {
|
||||||
this.$router.push({ name: 'createAccount' })
|
this.$router.push({ name: 'createAccount' })
|
||||||
}
|
}
|
||||||
else if( this.$root.appSettings.useDirectCapture && this.$root.appSettings.defaultCaptureMode === 'livescan' ) {
|
else if( this.$root.userPreferences.useDirectCapture && this.$root.userPreferences.defaultCaptureMode === 'livescan' ) {
|
||||||
this.$router.push({ name: 'capture' })
|
this.$router.push({ name: 'capture' })
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@ -541,10 +541,10 @@
|
|||||||
setActiveGroup(id) {
|
setActiveGroup(id) {
|
||||||
|
|
||||||
// In memomry saving
|
// In memomry saving
|
||||||
this.form.value = this.$root.appSettings.activeGroup = id
|
this.form.value = this.$root.userPreferences.activeGroup = id
|
||||||
|
|
||||||
// In db saving if the user set 2FAuth to memorize the active group
|
// In db saving if the user set 2FAuth to memorize the active group
|
||||||
if( this.$root.appSettings.rememberActiveGroup ) {
|
if( this.$root.userPreferences.rememberActiveGroup ) {
|
||||||
this.form.put('/api/v1/settings/activeGroup', {returnError: true})
|
this.form.put('/api/v1/settings/activeGroup', {returnError: true})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
// everything's fine
|
// everything's fine
|
||||||
|
@ -59,6 +59,12 @@
|
|||||||
'originalMessage' : this.$t('errors.auth_proxy_failed_legend')
|
'originalMessage' : this.$t('errors.auth_proxy_failed_legend')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (this.err.status === 403) {
|
||||||
|
return {
|
||||||
|
'message' : this.$t('errors.unauthorized'),
|
||||||
|
'originalMessage' : this.$t('errors.unauthorized_legend')
|
||||||
|
}
|
||||||
|
}
|
||||||
else if(this.err.data) {
|
else if(this.err.data) {
|
||||||
return this.err.data
|
return this.err.data
|
||||||
}
|
}
|
||||||
|
@ -107,8 +107,8 @@
|
|||||||
|
|
||||||
// Reset persisted group filter to 'All' (groupId=0)
|
// Reset persisted group filter to 'All' (groupId=0)
|
||||||
// (backend will save to change automatically)
|
// (backend will save to change automatically)
|
||||||
if( parseInt(this.$root.appSettings.activeGroup) === id ) {
|
if( parseInt(this.$root.userPreferences.activeGroup) === id ) {
|
||||||
this.$root.appSettings.activeGroup = 0
|
this.$root.userPreferences.activeGroup = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
<div class="column is-full quick-uploader-button" >
|
<div class="column is-full quick-uploader-button" >
|
||||||
<div class="quick-uploader-centerer">
|
<div class="quick-uploader-centerer">
|
||||||
<!-- upload a qr code (with basic file field and backend decoding) -->
|
<!-- upload a qr code (with basic file field and backend decoding) -->
|
||||||
<label role="button" tabindex="0" v-if="$root.appSettings.useBasicQrcodeReader" class="button is-link is-medium is-rounded is-main" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
|
<label role="button" tabindex="0" v-if="$root.userPreferences.useBasicQrcodeReader" class="button is-link is-medium is-rounded is-main" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
|
||||||
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
|
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
|
||||||
{{ $t('twofaccounts.forms.upload_qrcode') }}
|
{{ $t('twofaccounts.forms.upload_qrcode') }}
|
||||||
</label>
|
</label>
|
||||||
@ -25,7 +25,7 @@
|
|||||||
<div class="column is-full">
|
<div class="column is-full">
|
||||||
<div class="block" :class="$root.showDarkMode ? 'has-text-light':'has-text-grey-dark'">{{ $t('twofaccounts.forms.alternative_methods') }}</div>
|
<div class="block" :class="$root.showDarkMode ? 'has-text-light':'has-text-grey-dark'">{{ $t('twofaccounts.forms.alternative_methods') }}</div>
|
||||||
<!-- upload a qr code -->
|
<!-- upload a qr code -->
|
||||||
<div class="block has-text-link" v-if="!$root.appSettings.useBasicQrcodeReader">
|
<div class="block has-text-link" v-if="!$root.userPreferences.useBasicQrcodeReader">
|
||||||
<label role="button" tabindex="0" class="button is-link is-outlined is-rounded" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
|
<label role="button" tabindex="0" class="button is-link is-outlined is-rounded" ref="qrcodeInputLabel" @keyup.enter="$refs.qrcodeInputLabel.click()">
|
||||||
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
|
<input aria-hidden="true" tabindex="-1" class="file-input" type="file" accept="image/*" v-on:change="submitQrCode" ref="qrcodeInput">
|
||||||
{{ $t('twofaccounts.forms.upload_qrcode') }}
|
{{ $t('twofaccounts.forms.upload_qrcode') }}
|
||||||
@ -109,7 +109,7 @@
|
|||||||
created() {
|
created() {
|
||||||
|
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if( this.$root.appSettings.useDirectCapture && this.$root.appSettings.defaultCaptureMode === 'upload' ) {
|
if( this.$root.userPreferences.useDirectCapture && this.$root.userPreferences.defaultCaptureMode === 'upload' ) {
|
||||||
this.$refs.qrcodeInputLabel.click()
|
this.$refs.qrcodeInputLabel.click()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<p>{{ $t('auth.webauthn.lost_your_device') }} <router-link id="lnkRecoverAccount" :to="{ name: 'webauthn.lost' }" class="is-link">{{ $t('auth.webauthn.recover_your_account') }}</router-link></p>
|
<p>{{ $t('auth.webauthn.lost_your_device') }} <router-link id="lnkRecoverAccount" :to="{ name: 'webauthn.lost' }" class="is-link">{{ $t('auth.webauthn.recover_your_account') }}</router-link></p>
|
||||||
<p v-if="!this.$root.appSettings.useWebauthnOnly">{{ $t('auth.sign_in_using') }}
|
<p v-if="!this.$root.userPreferences.useWebauthnOnly">{{ $t('auth.sign_in_using') }}
|
||||||
<a id="lnkSignWithLegacy" role="button" class="is-link" @keyup.enter="showWebauthn = false" @click="showWebauthn = false" tabindex="0">{{ $t('auth.login_and_password') }}</a>
|
<a id="lnkSignWithLegacy" role="button" class="is-link" @keyup.enter="showWebauthn = false" @click="showWebauthn = false" tabindex="0">{{ $t('auth.login_and_password') }}</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -57,7 +57,7 @@
|
|||||||
password: ''
|
password: ''
|
||||||
}),
|
}),
|
||||||
isBusy: false,
|
isBusy: false,
|
||||||
showWebauthn: this.$root.appSettings.useWebauthnAsDefault || this.$root.appSettings.useWebauthnOnly,
|
showWebauthn: this.$root.userPreferences.useWebauthnAsDefault || this.$root.userPreferences.useWebauthnOnly,
|
||||||
csrfRefresher: null,
|
csrfRefresher: null,
|
||||||
webauthn: new WebAuthn()
|
webauthn: new WebAuthn()
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,9 @@
|
|||||||
<setting-tabs :activeTab="'settings.account'"></setting-tabs>
|
<setting-tabs :activeTab="'settings.account'"></setting-tabs>
|
||||||
<div class="options-tabs">
|
<div class="options-tabs">
|
||||||
<form-wrapper>
|
<form-wrapper>
|
||||||
|
<div v-if="isAdmin" class="notification is-warning">
|
||||||
|
{{ $t('settings.you_are_administrator') }}
|
||||||
|
</div>
|
||||||
<form @submit.prevent="submitProfile" @keydown="formProfile.onKeydown($event)">
|
<form @submit.prevent="submitProfile" @keydown="formProfile.onKeydown($event)">
|
||||||
<div v-if="isRemoteUser" class="notification is-warning has-text-centered" v-html="$t('auth.user_account_controlled_by_proxy')" />
|
<div v-if="isRemoteUser" class="notification is-warning has-text-centered" v-html="$t('auth.user_account_controlled_by_proxy')" />
|
||||||
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.profile') }}</h4>
|
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.profile') }}</h4>
|
||||||
@ -66,12 +69,14 @@
|
|||||||
password : '',
|
password : '',
|
||||||
}),
|
}),
|
||||||
isRemoteUser: false,
|
isRemoteUser: false,
|
||||||
|
isAdmin: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const { data } = await this.formProfile.get('/api/v1/user')
|
const { data } = await this.formProfile.get('/api/v1/user')
|
||||||
|
|
||||||
|
if( data.is_admin === true ) this.isAdmin = true
|
||||||
if( data.id === null ) this.isRemoteUser = true
|
if( data.id === null ) this.isRemoteUser = true
|
||||||
|
|
||||||
this.formProfile.fill(data)
|
this.formProfile.fill(data)
|
||||||
|
@ -3,58 +3,65 @@
|
|||||||
<setting-tabs :activeTab="'settings.options'"></setting-tabs>
|
<setting-tabs :activeTab="'settings.options'"></setting-tabs>
|
||||||
<div class="options-tabs">
|
<div class="options-tabs">
|
||||||
<form-wrapper>
|
<form-wrapper>
|
||||||
<!-- <form @submit.prevent="handleSubmit" @change="handleSubmit" @keydown="form.onKeydown($event)"> -->
|
|
||||||
<form>
|
<form>
|
||||||
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
|
<!-- user preferences -->
|
||||||
<!-- Check for update -->
|
<div class="block">
|
||||||
<form-checkbox v-on:checkForUpdate="saveSetting('checkForUpdate', $event)" :form="form" fieldName="checkForUpdate" :label="$t('commons.check_for_update')" :help="$t('commons.check_for_update_help')" />
|
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
|
||||||
<version-checker></version-checker>
|
<!-- Language -->
|
||||||
<!-- Language -->
|
<form-select v-on:lang="savePreference('lang', $event)" :options="langs" :form="preferencesForm" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
|
||||||
<form-select v-on:lang="saveSetting('lang', $event)" :options="langs" :form="form" fieldName="lang" :label="$t('settings.forms.language.label')" :help="$t('settings.forms.language.help')" />
|
<div class="field help">
|
||||||
<div class="field help">
|
{{ $t('settings.forms.some_translation_are_missing') }}
|
||||||
{{ $t('settings.forms.some_translation_are_missing') }}
|
<a class="ml-2" href="https://crowdin.com/project/2fauth">
|
||||||
<a class="ml-2" href="https://crowdin.com/project/2fauth">
|
{{ $t('settings.forms.help_translate_2fauth') }}
|
||||||
{{ $t('settings.forms.help_translate_2fauth') }}
|
<font-awesome-icon :icon="['fas', 'external-link-alt']" />
|
||||||
<font-awesome-icon :icon="['fas', 'external-link-alt']" />
|
</a>
|
||||||
</a>
|
</div>
|
||||||
|
<!-- display mode -->
|
||||||
|
<form-toggle v-on:displayMode="savePreference('displayMode', $event)" :choices="layouts" :form="preferencesForm" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
|
||||||
|
<!-- theme -->
|
||||||
|
<form-toggle v-on:theme="savePreference('theme', $event)" :choices="themes" :form="preferencesForm" fieldName="theme" :label="$t('settings.forms.theme.label')" :help="$t('settings.forms.theme.help')" />
|
||||||
|
<!-- show icon -->
|
||||||
|
<form-checkbox v-on:showAccountsIcons="savePreference('showAccountsIcons', $event)" :form="preferencesForm" fieldName="showAccountsIcons" :label="$t('settings.forms.show_accounts_icons.label')" :help="$t('settings.forms.show_accounts_icons.help')" />
|
||||||
|
<!-- Official icons -->
|
||||||
|
<form-checkbox v-on:getOfficialIcons="savePreference('getOfficialIcons', $event)" :form="preferencesForm" fieldName="getOfficialIcons" :label="$t('settings.forms.get_official_icons.label')" :help="$t('settings.forms.get_official_icons.help')" />
|
||||||
|
<!-- password format -->
|
||||||
|
<form-checkbox v-on:formatPassword="savePreference('formatPassword', $event)" :form="preferencesForm" fieldName="formatPassword" :label="$t('settings.forms.password_format.label')" :help="$t('settings.forms.password_format.help')" />
|
||||||
|
<form-toggle v-if="preferencesForm.formatPassword" v-on:formatPasswordBy="savePreference('formatPasswordBy', $event)" :choices="passwordFormats" :form="preferencesForm" fieldName="formatPasswordBy" />
|
||||||
|
|
||||||
|
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('groups.groups') }}</h4>
|
||||||
|
<!-- default group -->
|
||||||
|
<form-select v-on:defaultGroup="savePreference('defaultGroup', $event)" :options="groups" :form="preferencesForm" fieldName="defaultGroup" :label="$t('settings.forms.default_group.label')" :help="$t('settings.forms.default_group.help')" />
|
||||||
|
<!-- retain active group -->
|
||||||
|
<form-checkbox v-on:rememberActiveGroup="savePreference('rememberActiveGroup', $event)" :form="preferencesForm" fieldName="rememberActiveGroup" :label="$t('settings.forms.remember_active_group.label')" :help="$t('settings.forms.remember_active_group.help')" />
|
||||||
|
|
||||||
|
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
|
||||||
|
<!-- auto lock -->
|
||||||
|
<form-select v-on:kickUserAfter="savePreference('kickUserAfter', $event)" :options="kickUserAfters" :form="preferencesForm" fieldName="kickUserAfter" :label="$t('settings.forms.auto_lock.label')" :help="$t('settings.forms.auto_lock.help')" />
|
||||||
|
<!-- otp as dot -->
|
||||||
|
<form-checkbox v-on:showOtpAsDot="savePreference('showOtpAsDot', $event)" :form="preferencesForm" fieldName="showOtpAsDot" :label="$t('settings.forms.show_otp_as_dot.label')" :help="$t('settings.forms.show_otp_as_dot.help')" />
|
||||||
|
<!-- close otp on copy -->
|
||||||
|
<form-checkbox v-on:closeOtpOnCopy="savePreference('closeOtpOnCopy', $event)" :form="preferencesForm" fieldName="closeOtpOnCopy" :label="$t('settings.forms.close_otp_on_copy.label')" :help="$t('settings.forms.close_otp_on_copy.help')" />
|
||||||
|
<!-- copy otp on get -->
|
||||||
|
<form-checkbox v-on:copyOtpOnDisplay="savePreference('copyOtpOnDisplay', $event)" :form="preferencesForm" fieldName="copyOtpOnDisplay" :label="$t('settings.forms.copy_otp_on_display.label')" :help="$t('settings.forms.copy_otp_on_display.help')" />
|
||||||
|
|
||||||
|
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
|
||||||
|
<!-- basic qrcode -->
|
||||||
|
<form-checkbox v-on:useBasicQrcodeReader="savePreference('useBasicQrcodeReader', $event)" :form="preferencesForm" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
|
||||||
|
<!-- direct capture -->
|
||||||
|
<form-checkbox v-on:useDirectCapture="savePreference('useDirectCapture', $event)" :form="preferencesForm" fieldName="useDirectCapture" :label="$t('settings.forms.useDirectCapture.label')" :help="$t('settings.forms.useDirectCapture.help')" />
|
||||||
|
<!-- default capture mode -->
|
||||||
|
<form-select v-on:defaultCaptureMode="savePreference('defaultCaptureMode', $event)" :options="captureModes" :form="preferencesForm" fieldName="defaultCaptureMode" :label="$t('settings.forms.defaultCaptureMode.label')" :help="$t('settings.forms.defaultCaptureMode.help')" />
|
||||||
|
</div>
|
||||||
|
<!-- Admin settings -->
|
||||||
|
<div v-if="settingsForm">
|
||||||
|
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.administration') }}</h4>
|
||||||
|
<div class="is-size-7-mobile block" v-html="$t('settings.administration_legend')"></div>
|
||||||
|
<!-- Check for update -->
|
||||||
|
<form-checkbox v-on:checkForUpdate="saveSetting('checkForUpdate', $event)" :form="settingsForm" fieldName="checkForUpdate" :label="$t('commons.check_for_update')" :help="$t('commons.check_for_update_help')" />
|
||||||
|
<version-checker></version-checker>
|
||||||
|
<!-- protect db -->
|
||||||
|
<form-checkbox v-on:useEncryption="saveSetting('useEncryption', $event)" :form="settingsForm" fieldName="useEncryption" :label="$t('settings.forms.use_encryption.label')" :help="$t('settings.forms.use_encryption.help')" />
|
||||||
</div>
|
</div>
|
||||||
<!-- display mode -->
|
|
||||||
<form-toggle v-on:displayMode="saveSetting('displayMode', $event)" :choices="layouts" :form="form" fieldName="displayMode" :label="$t('settings.forms.display_mode.label')" :help="$t('settings.forms.display_mode.help')" />
|
|
||||||
<!-- theme -->
|
|
||||||
<form-toggle v-on:theme="saveSetting('theme', $event)" :choices="themes" :form="form" fieldName="theme" :label="$t('settings.forms.theme.label')" :help="$t('settings.forms.theme.help')" />
|
|
||||||
<!-- show icon -->
|
|
||||||
<form-checkbox v-on:showAccountsIcons="saveSetting('showAccountsIcons', $event)" :form="form" fieldName="showAccountsIcons" :label="$t('settings.forms.show_accounts_icons.label')" :help="$t('settings.forms.show_accounts_icons.help')" />
|
|
||||||
<!-- Official icons -->
|
|
||||||
<form-checkbox v-on:getOfficialIcons="saveSetting('getOfficialIcons', $event)" :form="form" fieldName="getOfficialIcons" :label="$t('settings.forms.get_official_icons.label')" :help="$t('settings.forms.get_official_icons.help')" />
|
|
||||||
<!-- password format -->
|
|
||||||
<form-checkbox v-on:formatPassword="saveSetting('formatPassword', $event)" :form="form" fieldName="formatPassword" :label="$t('settings.forms.password_format.label')" :help="$t('settings.forms.password_format.help')" />
|
|
||||||
<form-toggle v-if="form.formatPassword" v-on:formatPasswordBy="saveSetting('formatPasswordBy', $event)" :choices="passwordFormats" :form="form" fieldName="formatPasswordBy" />
|
|
||||||
|
|
||||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('groups.groups') }}</h4>
|
|
||||||
<!-- default group -->
|
|
||||||
<form-select v-on:defaultGroup="saveSetting('defaultGroup', $event)" :options="groups" :form="form" fieldName="defaultGroup" :label="$t('settings.forms.default_group.label')" :help="$t('settings.forms.default_group.help')" />
|
|
||||||
<!-- retain active group -->
|
|
||||||
<form-checkbox v-on:rememberActiveGroup="saveSetting('rememberActiveGroup', $event)" :form="form" fieldName="rememberActiveGroup" :label="$t('settings.forms.remember_active_group.label')" :help="$t('settings.forms.remember_active_group.help')" />
|
|
||||||
|
|
||||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
|
|
||||||
<!-- auto lock -->
|
|
||||||
<form-select v-on:kickUserAfter="saveSetting('kickUserAfter', $event)" :options="kickUserAfters" :form="form" fieldName="kickUserAfter" :label="$t('settings.forms.auto_lock.label')" :help="$t('settings.forms.auto_lock.help')" />
|
|
||||||
<!-- protect db -->
|
|
||||||
<form-checkbox v-on:useEncryption="saveSetting('useEncryption', $event)" :form="form" fieldName="useEncryption" :label="$t('settings.forms.use_encryption.label')" :help="$t('settings.forms.use_encryption.help')" />
|
|
||||||
<!-- otp as dot -->
|
|
||||||
<form-checkbox v-on:showOtpAsDot="saveSetting('showOtpAsDot', $event)" :form="form" fieldName="showOtpAsDot" :label="$t('settings.forms.show_otp_as_dot.label')" :help="$t('settings.forms.show_otp_as_dot.help')" />
|
|
||||||
<!-- close otp on copy -->
|
|
||||||
<form-checkbox v-on:closeOtpOnCopy="saveSetting('closeOtpOnCopy', $event)" :form="form" fieldName="closeOtpOnCopy" :label="$t('settings.forms.close_otp_on_copy.label')" :help="$t('settings.forms.close_otp_on_copy.help')" />
|
|
||||||
<!-- copy otp on get -->
|
|
||||||
<form-checkbox v-on:copyOtpOnDisplay="saveSetting('copyOtpOnDisplay', $event)" :form="form" fieldName="copyOtpOnDisplay" :label="$t('settings.forms.copy_otp_on_display.label')" :help="$t('settings.forms.copy_otp_on_display.help')" />
|
|
||||||
|
|
||||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
|
|
||||||
<!-- basic qrcode -->
|
|
||||||
<form-checkbox v-on:useBasicQrcodeReader="saveSetting('useBasicQrcodeReader', $event)" :form="form" fieldName="useBasicQrcodeReader" :label="$t('settings.forms.use_basic_qrcode_reader.label')" :help="$t('settings.forms.use_basic_qrcode_reader.help')" />
|
|
||||||
<!-- direct capture -->
|
|
||||||
<form-checkbox v-on:useDirectCapture="saveSetting('useDirectCapture', $event)" :form="form" fieldName="useDirectCapture" :label="$t('settings.forms.useDirectCapture.label')" :help="$t('settings.forms.useDirectCapture.help')" />
|
|
||||||
<!-- default capture mode -->
|
|
||||||
<form-select v-on:defaultCaptureMode="saveSetting('defaultCaptureMode', $event)" :options="captureModes" :form="form" fieldName="defaultCaptureMode" :label="$t('settings.forms.defaultCaptureMode.label')" :help="$t('settings.forms.defaultCaptureMode.help')" />
|
|
||||||
</form>
|
</form>
|
||||||
</form-wrapper>
|
</form-wrapper>
|
||||||
</div>
|
</div>
|
||||||
@ -93,26 +100,29 @@
|
|||||||
export default {
|
export default {
|
||||||
data(){
|
data(){
|
||||||
return {
|
return {
|
||||||
form: new Form({
|
preferencesForm: new Form({
|
||||||
lang: 'browser',
|
lang: '',
|
||||||
showOtpAsDot: null,
|
showOtpAsDot: null,
|
||||||
closeOtpOnCopy: null,
|
closeOtpOnCopy: null,
|
||||||
copyOtpOnDisplay: null,
|
copyOtpOnDisplay: null,
|
||||||
useBasicQrcodeReader: null,
|
useBasicQrcodeReader: null,
|
||||||
showAccountsIcons: null,
|
showAccountsIcons: null,
|
||||||
displayMode: '',
|
displayMode: '',
|
||||||
kickUserAfter: '',
|
kickUserAfter: null,
|
||||||
useEncryption: null,
|
|
||||||
defaultGroup: '',
|
defaultGroup: '',
|
||||||
useDirectCapture: null,
|
useDirectCapture: null,
|
||||||
defaultCaptureMode: '',
|
defaultCaptureMode: '',
|
||||||
rememberActiveGroup: true,
|
rememberActiveGroup: null,
|
||||||
getOfficialIcons: null,
|
getOfficialIcons: null,
|
||||||
checkForUpdate: null,
|
theme: '',
|
||||||
theme: 'dark',
|
|
||||||
formatPassword: null,
|
formatPassword: null,
|
||||||
formatPasswordBy: '',
|
formatPasswordBy: '',
|
||||||
}),
|
}),
|
||||||
|
settingsForm: null,
|
||||||
|
settings: {
|
||||||
|
useEncryption: null,
|
||||||
|
checkForUpdate: null,
|
||||||
|
},
|
||||||
layouts: [
|
layouts: [
|
||||||
{ text: this.$t('settings.forms.grid'), value: 'grid', icon: 'th' },
|
{ text: this.$t('settings.forms.grid'), value: 'grid', icon: 'th' },
|
||||||
{ text: this.$t('settings.forms.list'), value: 'list', icon: 'list' },
|
{ text: this.$t('settings.forms.list'), value: 'list', icon: 'list' },
|
||||||
@ -128,15 +138,15 @@
|
|||||||
{ text: '1234 5678', value: 0.5, legend: this.$t('settings.forms.half'), title: this.$t('settings.forms.half_legend') },
|
{ text: '1234 5678', value: 0.5, legend: this.$t('settings.forms.half'), title: this.$t('settings.forms.half_legend') },
|
||||||
],
|
],
|
||||||
kickUserAfters: [
|
kickUserAfters: [
|
||||||
{ text: this.$t('settings.forms.never'), value: '0' },
|
{ text: this.$t('settings.forms.never'), value: 0 },
|
||||||
{ text: this.$t('settings.forms.on_otp_copy'), value: '-1' },
|
{ text: this.$t('settings.forms.on_otp_copy'), value: -1 },
|
||||||
{ text: this.$t('settings.forms.1_minutes'), 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.5_minutes'), value: 5 },
|
||||||
{ text: this.$t('settings.forms.10_minutes'), value: '10' },
|
{ text: this.$t('settings.forms.10_minutes'), value: 10 },
|
||||||
{ text: this.$t('settings.forms.15_minutes'), value: '15' },
|
{ text: this.$t('settings.forms.15_minutes'), value: 15 },
|
||||||
{ text: this.$t('settings.forms.30_minutes'), value: '30' },
|
{ text: this.$t('settings.forms.30_minutes'), value: 30 },
|
||||||
{ text: this.$t('settings.forms.1_hour'), value: '60' },
|
{ text: this.$t('settings.forms.1_hour'), value: 60 },
|
||||||
{ text: this.$t('settings.forms.1_day'), value: '1440' },
|
{ text: this.$t('settings.forms.1_day'), value: 1440 },
|
||||||
],
|
],
|
||||||
groups: [
|
groups: [
|
||||||
{ text: this.$t('groups.no_group'), value: 0 },
|
{ text: this.$t('groups.no_group'), value: 0 },
|
||||||
@ -157,7 +167,7 @@
|
|||||||
computed : {
|
computed : {
|
||||||
langs: function() {
|
langs: function() {
|
||||||
let locales = [{
|
let locales = [{
|
||||||
text: this.$t('languages.browser_preference') + ' (' + this.$root.$i18n.locale + ')',
|
text: this.$t('languages.browser_preference'),
|
||||||
value: 'browser'
|
value: 'browser'
|
||||||
}];
|
}];
|
||||||
|
|
||||||
@ -172,39 +182,41 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const { data } = await this.form.get('/api/v1/settings')
|
|
||||||
|
|
||||||
this.form.fillWithKeyValueObject(data)
|
const preferences = await this.preferencesForm.get('/api/v1/user/preferences')
|
||||||
let lang = data.filter(x => x.key === 'lang')
|
this.preferencesForm.fillWithKeyValueObject(preferences.data)
|
||||||
|
this.preferencesForm.setOriginal()
|
||||||
|
|
||||||
if (lang.value == 'browser') {
|
this.axios.get('/api/v1/settings', {returnError: true}).then(response => {
|
||||||
if(window.appLocales.includes(lang.value)) {
|
this.settingsForm = new Form(this.settings)
|
||||||
this.form.lang = lang
|
this.settingsForm.fillWithKeyValueObject(response.data)
|
||||||
}
|
this.settingsForm.setOriginal()
|
||||||
}
|
})
|
||||||
// this.$root.$i18n.locale
|
.catch(error => {
|
||||||
|
// no admin rights, we do not set the Settings form
|
||||||
|
})
|
||||||
|
|
||||||
this.form.setOriginal()
|
|
||||||
this.fetchGroups()
|
this.fetchGroups()
|
||||||
},
|
},
|
||||||
|
|
||||||
methods : {
|
methods : {
|
||||||
handleSubmit(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
console.log(e)
|
|
||||||
|
|
||||||
// this.form.post('/api/v1/settings/options', {returnError: false})
|
savePreference(preferenceName, event) {
|
||||||
// .then(response => {
|
|
||||||
|
|
||||||
// this.$notify({ type: 'is-success', text: response.data.message })
|
this.axios.put('/api/v1/user/preferences/' + preferenceName, { value: event }).then(response => {
|
||||||
|
this.$notify({ type: 'is-success', text: this.$t('settings.forms.setting_saved') })
|
||||||
|
|
||||||
// if(response.data.settings.lang !== this.$root.$i18n.locale) {
|
if(preferenceName === 'lang' && response.data.value !== this.$root.$i18n.locale) {
|
||||||
// this.$router.go()
|
this.$router.go()
|
||||||
// }
|
}
|
||||||
// else {
|
else {
|
||||||
// this.$root.appSettings = response.data.settings
|
this.$root.userPreferences[response.data.key] = response.data.value
|
||||||
// }
|
|
||||||
// });
|
if(preferenceName === 'theme') {
|
||||||
|
this.setTheme(response.data.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
saveSetting(settingName, event) {
|
saveSetting(settingName, event) {
|
||||||
@ -212,16 +224,7 @@
|
|||||||
this.axios.put('/api/v1/settings/' + settingName, { value: event }).then(response => {
|
this.axios.put('/api/v1/settings/' + settingName, { value: event }).then(response => {
|
||||||
this.$notify({ type: 'is-success', text: this.$t('settings.forms.setting_saved') })
|
this.$notify({ type: 'is-success', text: this.$t('settings.forms.setting_saved') })
|
||||||
|
|
||||||
if(settingName === 'lang' && response.data.value !== this.$root.$i18n.locale) {
|
this.$root.appSettings[response.data.key] = response.data.value
|
||||||
this.$router.go()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
this.$root.appSettings[response.data.key] = response.data.value
|
|
||||||
|
|
||||||
if(settingName === 'theme') {
|
|
||||||
this.setTheme(response.data.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -92,9 +92,9 @@
|
|||||||
* Save a setting
|
* Save a setting
|
||||||
*/
|
*/
|
||||||
saveSetting(settingName, event) {
|
saveSetting(settingName, event) {
|
||||||
this.axios.put('/api/v1/settings/' + settingName, { value: event }).then(response => {
|
this.axios.put('/api/v1/user/preferences/' + settingName, { value: event }).then(response => {
|
||||||
this.$notify({ type: 'is-success', text: this.$t('settings.forms.setting_saved') })
|
this.$notify({ type: 'is-success', text: this.$t('settings.forms.setting_saved') })
|
||||||
this.$root.appSettings[response.data.key] = response.data.value
|
this.$root.userPreferences[response.data.key] = response.data.value
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -191,8 +191,8 @@
|
|||||||
if (this.credentials.length == 0) {
|
if (this.credentials.length == 0) {
|
||||||
this.form.useWebauthnOnly = false
|
this.form.useWebauthnOnly = false
|
||||||
this.form.useWebauthnAsDefault = false
|
this.form.useWebauthnAsDefault = false
|
||||||
this.$root.appSettings['useWebauthnOnly'] = false
|
this.$root.userPreferences['useWebauthnOnly'] = false
|
||||||
this.$root.appSettings['useWebauthnAsDefault'] = false
|
this.$root.userPreferences['useWebauthnAsDefault'] = false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$notify({ type: 'is-success', text: this.$t('auth.webauthn.device_revoked') })
|
this.$notify({ type: 'is-success', text: this.$t('auth.webauthn.device_revoked') })
|
||||||
|
@ -65,7 +65,7 @@
|
|||||||
<label class="label">{{ $t('twofaccounts.icon') }}</label>
|
<label class="label">{{ $t('twofaccounts.icon') }}</label>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<!-- i'm lucky button -->
|
<!-- i'm lucky button -->
|
||||||
<div class="control" v-if="$root.appSettings.getOfficialIcons">
|
<div class="control" v-if="$root.userPreferences.getOfficialIcons">
|
||||||
<v-button @click="fetchLogo" :color="$root.showDarkMode ? 'is-dark' : ''" :nativeType="'button'" :isDisabled="form.service.length < 1">
|
<v-button @click="fetchLogo" :color="$root.showDarkMode ? 'is-dark' : ''" :nativeType="'button'" :isDisabled="form.service.length < 1">
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<font-awesome-icon :icon="['fas', 'globe']" />
|
<font-awesome-icon :icon="['fas', 'globe']" />
|
||||||
@ -94,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<field-error :form="form" field="icon" class="help-for-file" />
|
<field-error :form="form" field="icon" class="help-for-file" />
|
||||||
<p v-if="$root.appSettings.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
|
<p v-if="$root.userPreferences.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
|
||||||
</div>
|
</div>
|
||||||
<!-- otp type -->
|
<!-- otp type -->
|
||||||
<form-toggle class="has-uppercased-button" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
|
<form-toggle class="has-uppercased-button" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
|
||||||
@ -365,7 +365,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
fetchLogo() {
|
fetchLogo() {
|
||||||
if (this.$root.appSettings.getOfficialIcons) {
|
if (this.$root.userPreferences.getOfficialIcons) {
|
||||||
this.axios.post('/api/v1/icons/default', {service: this.form.service}, {returnError: true}).then(response => {
|
this.axios.post('/api/v1/icons/default', {service: this.form.service}, {returnError: true}).then(response => {
|
||||||
if (response.status === 201) {
|
if (response.status === 201) {
|
||||||
// clean possible already uploaded temp icon
|
// clean possible already uploaded temp icon
|
||||||
@ -397,7 +397,7 @@
|
|||||||
|
|
||||||
clipboardSuccessHandler ({ value, event }) {
|
clipboardSuccessHandler ({ value, event }) {
|
||||||
|
|
||||||
if(this.$root.appSettings.kickUserAfter == -1) {
|
if(this.$root.appSettings.userPreferences == -1) {
|
||||||
this.appLogout()
|
this.appLogout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<label class="label">{{ $t('twofaccounts.icon') }}</label>
|
<label class="label">{{ $t('twofaccounts.icon') }}</label>
|
||||||
<div class="field is-grouped">
|
<div class="field is-grouped">
|
||||||
<!-- i'm lucky button -->
|
<!-- i'm lucky button -->
|
||||||
<div class="control" v-if="$root.appSettings.getOfficialIcons">
|
<div class="control" v-if="$root.userPreferences.getOfficialIcons">
|
||||||
<v-button @click="fetchLogo" :color="$root.showDarkMode ? 'is-dark' : ''" :nativeType="'button'" :isDisabled="form.service.length < 3">
|
<v-button @click="fetchLogo" :color="$root.showDarkMode ? 'is-dark' : ''" :nativeType="'button'" :isDisabled="form.service.length < 3">
|
||||||
<span class="icon is-small">
|
<span class="icon is-small">
|
||||||
<font-awesome-icon :icon="['fas', 'globe']" />
|
<font-awesome-icon :icon="['fas', 'globe']" />
|
||||||
@ -38,7 +38,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<field-error :form="form" field="icon" class="help-for-file" />
|
<field-error :form="form" field="icon" class="help-for-file" />
|
||||||
<p v-if="$root.appSettings.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
|
<p v-if="$root.userPreferences.getOfficialIcons" class="help" v-html="$t('twofaccounts.forms.i_m_lucky_legend')"></p>
|
||||||
</div>
|
</div>
|
||||||
<!-- otp type -->
|
<!-- otp type -->
|
||||||
<form-toggle class="has-uppercased-button" :isDisabled="true" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
|
<form-toggle class="has-uppercased-button" :isDisabled="true" :form="form" :choices="otp_types" fieldName="otp_type" :label="$t('twofaccounts.forms.otp_type.label')" :help="$t('twofaccounts.forms.otp_type.help')" :hasOffset="true" />
|
||||||
@ -258,7 +258,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
fetchLogo() {
|
fetchLogo() {
|
||||||
if (this.$root.appSettings.getOfficialIcons) {
|
if (this.$root.userPreferences.getOfficialIcons) {
|
||||||
this.axios.post('/api/v1/icons/default', {service: this.form.service}, {returnError: true}).then(response => {
|
this.axios.post('/api/v1/icons/default', {service: this.form.service}, {returnError: true}).then(response => {
|
||||||
if (response.status === 201) {
|
if (response.status === 201) {
|
||||||
// clean possible already uploaded temp icon
|
// clean possible already uploaded temp icon
|
||||||
|
@ -55,7 +55,7 @@
|
|||||||
<div class="is-flex is-justify-content-space-between">
|
<div class="is-flex is-justify-content-space-between">
|
||||||
<!-- Account name -->
|
<!-- Account name -->
|
||||||
<div v-if="account.id > -2 && account.imported !== 0" class="is-flex-grow-1 has-ellipsis is-clickable" @click="previewAccount(index)" :title="$t('twofaccounts.import.generate_a_test_password')">
|
<div v-if="account.id > -2 && account.imported !== 0" class="is-flex-grow-1 has-ellipsis is-clickable" @click="previewAccount(index)" :title="$t('twofaccounts.import.generate_a_test_password')">
|
||||||
<img v-if="account.icon && $root.appSettings.showAccountsIcons" class="import-icon" :src="$root.appConfig.subdirectory + '/storage/icons/' + account.icon" :alt="$t('twofaccounts.icon_for_account_x_at_service_y', {account: account.account, service: account.service})">
|
<img v-if="account.icon && $root.userPreferences.showAccountsIcons" class="import-icon" :src="$root.appConfig.subdirectory + '/storage/icons/' + account.icon" :alt="$t('twofaccounts.icon_for_account_x_at_service_y', {account: account.account, service: account.service})">
|
||||||
{{ account.account }}
|
{{ account.account }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="is-flex-grow-1 has-ellipsis">{{ account.account }}</div>
|
<div v-else class="is-flex-grow-1 has-ellipsis">{{ account.account }}</div>
|
||||||
|
@ -47,5 +47,7 @@ return [
|
|||||||
'unsupported_otp_type' => 'Unsupported OTP type',
|
'unsupported_otp_type' => 'Unsupported OTP type',
|
||||||
'encrypted_migration' => 'Unreadable, the data seem encrypted',
|
'encrypted_migration' => 'Unreadable, the data seem encrypted',
|
||||||
'no_logo_found_for_x' => 'No logo available for {service}',
|
'no_logo_found_for_x' => 'No logo available for {service}',
|
||||||
'file_upload_failed' => 'File upload failed'
|
'file_upload_failed' => 'File upload failed',
|
||||||
|
'unauthorized' => 'Unauthorized',
|
||||||
|
'unauthorized_legend' => 'You do not have permissions to view this resource or to perform this action',
|
||||||
];
|
];
|
@ -14,6 +14,7 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'settings' => 'Settings',
|
'settings' => 'Settings',
|
||||||
|
'preferences' => 'Preferences',
|
||||||
'account' => 'Account',
|
'account' => 'Account',
|
||||||
'oauth' => 'OAuth',
|
'oauth' => 'OAuth',
|
||||||
'webauthn' => 'WebAuthn',
|
'webauthn' => 'WebAuthn',
|
||||||
@ -23,6 +24,9 @@ return [
|
|||||||
'confirm' => [
|
'confirm' => [
|
||||||
|
|
||||||
],
|
],
|
||||||
|
'administration' => 'Administration',
|
||||||
|
'administration_legend' => 'While previous settings are user settings (every user can set its own preferences), following settings are global and apply to all users. Only an administrator can view and edit those settings.',
|
||||||
|
'you_are_administrator' => 'You are an administrator',
|
||||||
'general' => 'General',
|
'general' => 'General',
|
||||||
'security' => 'Security',
|
'security' => 'Security',
|
||||||
'profile' => 'Profile',
|
'profile' => 'Profile',
|
||||||
|
5
resources/views/landing.blade.php
vendored
5
resources/views/landing.blade.php
vendored
@ -1,9 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html data-theme='{{ $theme }}' lang="{!! $lang !!}">
|
<html data-theme="{{ $userPreferences['theme'] }}" lang="{{ $userPreferences['lang'] }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="description" content="{{ __('commons.2fauth_description') }}" lang="{!! $lang !!}">
|
<meta name="description" content="{{ __('commons.2fauth_description') }}" lang="{{ $userPreferences['lang'] }}">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, shrink-to-fit=no, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0, shrink-to-fit=no, viewport-fit=cover">
|
||||||
<meta name="csrf-token" content="{{csrf_token()}}">
|
<meta name="csrf-token" content="{{csrf_token()}}">
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
@ -26,6 +26,7 @@
|
|||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
var appSettings = {!! $appSettings !!};
|
var appSettings = {!! $appSettings !!};
|
||||||
var appConfig = {!! $appConfig !!};
|
var appConfig = {!! $appConfig !!};
|
||||||
|
var userPreferences = {!! $userPreferences->toJson() !!};
|
||||||
var appVersion = '{{ config("2fauth.version") }}';
|
var appVersion = '{{ config("2fauth.version") }}';
|
||||||
var isDemoApp = {!! $isDemoApp !!};
|
var isDemoApp = {!! $isDemoApp !!};
|
||||||
var isTestingApp = {!! $isTestingApp !!};
|
var isTestingApp = {!! $isTestingApp !!};
|
||||||
|
@ -25,11 +25,9 @@ Route::get('user/name', [UserController::class, 'show'])->name('user.show.name')
|
|||||||
Route::group(['middleware' => 'auth:api-guard'], function () {
|
Route::group(['middleware' => 'auth:api-guard'], function () {
|
||||||
Route::get('user', [UserController::class, 'show'])->name('user.show'); // Returns email address in addition to the username
|
Route::get('user', [UserController::class, 'show'])->name('user.show'); // Returns email address in addition to the username
|
||||||
|
|
||||||
Route::get('settings/{settingName}', [SettingController::class, 'show'])->name('settings.show');
|
Route::get('user/preferences/{preferenceName}', [UserController::class, 'showPreference'])->name('user.preferences.show');
|
||||||
Route::get('settings', [SettingController::class, 'index'])->name('settings.index');
|
Route::get('user/preferences', [UserController::class, 'allPreferences'])->name('user.preferences.all');
|
||||||
Route::post('settings', [SettingController::class, 'store'])->name('settings.store');
|
Route::put('user/preferences/{preferenceName}', [UserController::class, 'setPreference'])->name('user.preferences.set');
|
||||||
Route::put('settings/{settingName}', [SettingController::class, 'update'])->name('settings.update');
|
|
||||||
Route::delete('settings/{settingName}', [SettingController::class, 'destroy'])->name('settings.destroy');
|
|
||||||
|
|
||||||
Route::delete('twofaccounts', [TwoFAccountController::class, 'batchDestroy'])->name('twofaccounts.batchDestroy');
|
Route::delete('twofaccounts', [TwoFAccountController::class, 'batchDestroy'])->name('twofaccounts.batchDestroy');
|
||||||
Route::patch('twofaccounts/withdraw', [TwoFAccountController::class, 'withdraw'])->name('twofaccounts.withdraw');
|
Route::patch('twofaccounts/withdraw', [TwoFAccountController::class, 'withdraw'])->name('twofaccounts.withdraw');
|
||||||
@ -53,3 +51,15 @@ Route::group(['middleware' => 'auth:api-guard'], function () {
|
|||||||
Route::post('icons', [IconController::class, 'upload'])->name('icons.upload');
|
Route::post('icons', [IconController::class, 'upload'])->name('icons.upload');
|
||||||
Route::delete('icons/{icon}', [IconController::class, 'delete'])->name('icons.delete');
|
Route::delete('icons/{icon}', [IconController::class, 'delete'])->name('icons.delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Routes protected by the api authentication guard and restricted to administrators
|
||||||
|
*/
|
||||||
|
Route::group(['middleware' => ['auth:api-guard', 'admin']], function () {
|
||||||
|
Route::get('settings/{settingName}', [SettingController::class, 'show'])->name('settings.show');
|
||||||
|
Route::get('settings', [SettingController::class, 'index'])->name('settings.index');
|
||||||
|
Route::post('settings', [SettingController::class, 'store'])->name('settings.store');
|
||||||
|
Route::put('settings/{settingName}', [SettingController::class, 'update'])->name('settings.update');
|
||||||
|
Route::delete('settings/{settingName}', [SettingController::class, 'destroy'])->name('settings.destroy');
|
||||||
|
|
||||||
|
});
|
||||||
|
@ -152,9 +152,9 @@ class SettingServiceTest extends FeatureTestCase
|
|||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function test_all_returns_native_and_user_settings()
|
public function test_all_returns_default_and_overloaded_settings()
|
||||||
{
|
{
|
||||||
$native_options = config('2fauth.options');
|
$default_options = config('2fauth.settings');
|
||||||
|
|
||||||
Settings::set(self::SETTING_NAME, self::SETTING_VALUE_STRING);
|
Settings::set(self::SETTING_NAME, self::SETTING_VALUE_STRING);
|
||||||
|
|
||||||
@ -163,12 +163,10 @@ class SettingServiceTest extends FeatureTestCase
|
|||||||
$this->assertArrayHasKey(self::SETTING_NAME, $all);
|
$this->assertArrayHasKey(self::SETTING_NAME, $all);
|
||||||
$this->assertEquals($all[self::SETTING_NAME], self::SETTING_VALUE_STRING);
|
$this->assertEquals($all[self::SETTING_NAME], self::SETTING_VALUE_STRING);
|
||||||
|
|
||||||
foreach ($native_options as $key => $val) {
|
foreach ($default_options as $key => $val) {
|
||||||
$this->assertArrayHasKey($key, $all);
|
$this->assertArrayHasKey($key, $all);
|
||||||
$this->assertEquals($all[$key], $val);
|
$this->assertEquals($all[$key], $val);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->assertArrayHasKey('lang', $all);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -347,23 +345,23 @@ class SettingServiceTest extends FeatureTestCase
|
|||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function test_isUserDefined_returns_true()
|
public function test_isEdited_returns_true()
|
||||||
{
|
{
|
||||||
DB::table('options')->insert(
|
DB::table('options')->insert(
|
||||||
[self::KEY => 'showTokenAsDot', self::VALUE => strval(self::SETTING_VALUE_TRUE_TRANSFORMED)]
|
[self::KEY => 'showTokenAsDot', self::VALUE => strval(self::SETTING_VALUE_TRUE_TRANSFORMED)]
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertTrue(Settings::isUserDefined('showTokenAsDot'));
|
$this->assertTrue(Settings::isEdited('showTokenAsDot'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function test_isUserDefined_returns_false()
|
public function test_isEdited_returns_false()
|
||||||
{
|
{
|
||||||
DB::table('options')->where(self::KEY, 'showTokenAsDot')->delete();
|
DB::table('options')->where(self::KEY, 'showTokenAsDot')->delete();
|
||||||
|
|
||||||
$this->assertFalse(Settings::isUserDefined('showTokenAsDot'));
|
$this->assertFalse(Settings::isEdited('showTokenAsDot'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
x
Reference in New Issue
Block a user