mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-06-20 19:57:44 +02:00
Add custom defaults & locking to user preferences - Closes #413
This commit is contained in:
parent
f6a595807d
commit
764b687904
12
.env.testing
12
.env.testing
@ -23,6 +23,18 @@ LOG_CHANNEL=stack
|
|||||||
DB_CONNECTION=testing
|
DB_CONNECTION=testing
|
||||||
DB_DATABASE=:memory:
|
DB_DATABASE=:memory:
|
||||||
|
|
||||||
|
# valid overriden default
|
||||||
|
USERPREF_DEFAULT__THEME=light
|
||||||
|
|
||||||
|
# valid locked user preference
|
||||||
|
USERPREF_LOCKED__THEME=true
|
||||||
|
|
||||||
|
# invalid locked user preference
|
||||||
|
USERPREF_LOCKED__DISPLAY_MODE=
|
||||||
|
|
||||||
|
# malformed locked user preference
|
||||||
|
USERPREF_LOCKED___FORMAT_PASSWORD=false
|
||||||
|
|
||||||
# DB_CONNECTION=mysql
|
# DB_CONNECTION=mysql
|
||||||
# DB_HOST=localhost
|
# DB_HOST=localhost
|
||||||
# DB_PORT=3306
|
# DB_PORT=3306
|
||||||
|
@ -33,8 +33,9 @@ class UserController extends Controller
|
|||||||
|
|
||||||
$preferences->each(function (mixed $item, string $key) use ($jsonPrefs) {
|
$preferences->each(function (mixed $item, string $key) use ($jsonPrefs) {
|
||||||
$jsonPrefs->push([
|
$jsonPrefs->push([
|
||||||
'key' => $key,
|
'key' => $key,
|
||||||
'value' => $item,
|
'value' => $item,
|
||||||
|
'locked' => in_array($key, config('2fauth.lockedPreferences')),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,8 +54,9 @@ class UserController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'key' => $preferenceName,
|
'key' => $preferenceName,
|
||||||
'value' => $request->user()->preferences[$preferenceName],
|
'value' => $request->user()->preferences[$preferenceName],
|
||||||
|
'locked' => in_array($preferenceName, config('2fauth.lockedPreferences')),
|
||||||
], 200);
|
], 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +71,10 @@ class UserController extends Controller
|
|||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (in_array($preferenceName, config('2fauth.lockedPreferences'))) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|
||||||
$request->user()['preferences->' . $preferenceName] = $validated['value'];
|
$request->user()['preferences->' . $preferenceName] = $validated['value'];
|
||||||
|
@ -49,4 +49,22 @@ class Helpers
|
|||||||
{
|
{
|
||||||
return Str::random($length) . '.' . $extension;
|
return Str::random($length) . '.' . $extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines preferences locked for change.
|
||||||
|
* This helper is only intended to be called from the 2FAuth config file.
|
||||||
|
*/
|
||||||
|
public static function lockedPreferences(array $preferences) : array
|
||||||
|
{
|
||||||
|
foreach ($preferences as $key => $value) {
|
||||||
|
$_key = $key === 'revealDottedOTP' ? 'revealDottedOtp' : $key;
|
||||||
|
$isLocked = envUnlessEmpty(Str::of($_key)->snake('_')->upper()->prepend('USERPREF_LOCKED__')->toString(), false);
|
||||||
|
|
||||||
|
if ($isLocked) {
|
||||||
|
$lockedPreferences[] = $key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $lockedPreferences ?? [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,7 @@ class SinglePageController extends Controller
|
|||||||
$proxyLogoutUrl = config('2fauth.config.proxyLogoutUrl') ? config('2fauth.config.proxyLogoutUrl') : false;
|
$proxyLogoutUrl = config('2fauth.config.proxyLogoutUrl') ? config('2fauth.config.proxyLogoutUrl') : false;
|
||||||
$subdir = config('2fauth.config.appSubdirectory') ? '/' . config('2fauth.config.appSubdirectory') : '';
|
$subdir = config('2fauth.config.appSubdirectory') ? '/' . config('2fauth.config.appSubdirectory') : '';
|
||||||
$defaultPreferences = collect(config('2fauth.preferences')); /** @phpstan-ignore-line */
|
$defaultPreferences = collect(config('2fauth.preferences')); /** @phpstan-ignore-line */
|
||||||
|
$lockedPreferences = collect(config('2fauth.lockedPreferences')); /** @phpstan-ignore-line */
|
||||||
$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';
|
||||||
$lang = App::getLocale();
|
$lang = App::getLocale();
|
||||||
@ -69,6 +70,7 @@ class SinglePageController extends Controller
|
|||||||
'exportSchemaUrl' => $exportSchemaUrl,
|
'exportSchemaUrl' => $exportSchemaUrl,
|
||||||
]),
|
]),
|
||||||
'defaultPreferences' => $defaultPreferences,
|
'defaultPreferences' => $defaultPreferences,
|
||||||
|
'lockedPreferences' => $lockedPreferences,
|
||||||
'subdirectory' => $subdir,
|
'subdirectory' => $subdir,
|
||||||
'isDemoApp' => $isDemoApp,
|
'isDemoApp' => $isDemoApp,
|
||||||
'isTestingApp' => $isTestingApp,
|
'isTestingApp' => $isTestingApp,
|
||||||
|
@ -13,15 +13,12 @@
|
|||||||
|
|
||||||
if (! function_exists('envUnlessEmpty')) {
|
if (! function_exists('envUnlessEmpty')) {
|
||||||
/**
|
/**
|
||||||
* @param string $key
|
|
||||||
* @param null $default
|
|
||||||
*
|
|
||||||
* @return mixed|null
|
* @return mixed|null
|
||||||
*/
|
*/
|
||||||
function envUnlessEmpty(string $key, $default = null)
|
function envUnlessEmpty(string $key, string|int|bool|float|null $default = null)
|
||||||
{
|
{
|
||||||
$result = env($key, $default);
|
$result = env($key, $default);
|
||||||
if (is_string($result) && '' === $result) {
|
if ('' === $result) {
|
||||||
$result = $default;
|
$result = $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,47 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Helpers\Helpers;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
|
||||||
|
$preferences = [
|
||||||
|
'showOtpAsDot' => envUnlessEmpty('USERPREF_DEFAULT__SHOW_OTP_AS_DOT', false),
|
||||||
|
'showNextOtp' => envUnlessEmpty('USERPREF_DEFAULT__SHOW_NEXT_OTP', false),
|
||||||
|
'revealDottedOTP' => envUnlessEmpty('USERPREF_DEFAULT__REVEAL_DOTTED_OTP', false),
|
||||||
|
'closeOtpOnCopy' => envUnlessEmpty('USERPREF_DEFAULT__CLOSE_OTP_ON_COPY', false),
|
||||||
|
'copyOtpOnDisplay' => envUnlessEmpty('USERPREF_DEFAULT__COPY_OTP_ON_DISPLAY', false),
|
||||||
|
'clearSearchOnCopy' => envUnlessEmpty('USERPREF_DEFAULT__CLEAR_SEARCH_ON_COPY', false),
|
||||||
|
'useBasicQrcodeReader' => envUnlessEmpty('USERPREF_DEFAULT__USE_BASIC_QRCODE_READER', false),
|
||||||
|
'displayMode' => envUnlessEmpty('USERPREF_DEFAULT__DISPLAY_MODE', 'list'),
|
||||||
|
'showAccountsIcons' => envUnlessEmpty('USERPREF_DEFAULT__SHOW_ACCOUNTS_ICONS', true),
|
||||||
|
'kickUserAfter' => envUnlessEmpty('USERPREF_DEFAULT__KICK_USER_AFTER', 15),
|
||||||
|
'activeGroup' => 0,
|
||||||
|
'rememberActiveGroup' => envUnlessEmpty('USERPREF_DEFAULT__REMEMBER_ACTIVE_GROUP', true),
|
||||||
|
'viewDefaultGroupOnCopy' => envUnlessEmpty('USERPREF_DEFAULT__VIEW_DEFAULT_GROUP_ON_COPY', false),
|
||||||
|
'defaultGroup' => 0,
|
||||||
|
'defaultCaptureMode' => envUnlessEmpty('USERPREF_DEFAULT__DEFAULT_CAPTURE_MODE', 'livescan'),
|
||||||
|
'useDirectCapture' => envUnlessEmpty('USERPREF_DEFAULT__USE_DIRECT_CAPTURE', false),
|
||||||
|
'useWebauthnOnly' => envUnlessEmpty('USERPREF_DEFAULT__USE_WEBAUTHN_ONLY', false),
|
||||||
|
'getOfficialIcons' => envUnlessEmpty('USERPREF_DEFAULT__GET_OFFICIAL_ICONS', true),
|
||||||
|
'theme' => envUnlessEmpty('USERPREF_DEFAULT__THEME', 'system'),
|
||||||
|
'formatPassword' => envUnlessEmpty('USERPREF_DEFAULT__FORMAT_PASSWORD', true),
|
||||||
|
'formatPasswordBy' => envUnlessEmpty('USERPREF_DEFAULT__FORMAT_PASSWORD_BY', 0.5),
|
||||||
|
'lang' => envUnlessEmpty('USERPREF_DEFAULT__LANG', 'browser'),
|
||||||
|
'getOtpOnRequest' => envUnlessEmpty('USERPREF_DEFAULT__GET_OTP_ON_REQUEST', true),
|
||||||
|
'notifyOnNewAuthDevice' => envUnlessEmpty('USERPREF_DEFAULT__NOTIFY_ON_NEW_AUTH_DEVICE', false),
|
||||||
|
'notifyOnFailedLogin' => envUnlessEmpty('USERPREF_DEFAULT__NOTIFY_ON_FAILED_LOGIN', false),
|
||||||
|
'timezone' => envUnlessEmpty('USERPREF_DEFAULT__TIMEZONE', 'UTC'),
|
||||||
|
'sortCaseSensitive' => envUnlessEmpty('USERPREF_DEFAULT__SORT_CASE_SENSITIVE', false),
|
||||||
|
'autoCloseTimeout' => envUnlessEmpty('USERPREF_DEFAULT__AUTO_CLOSE_TIMEOUT', 2),
|
||||||
|
'AutoSaveQrcodedAccount' => envUnlessEmpty('USERPREF_DEFAULT__AUTO_SAVE_QRCODED_ACCOUNT', false),
|
||||||
|
'showEmailInFooter' => envUnlessEmpty('USERPREF_DEFAULT__SHOW_EMAIL_IN_FOOTER', true),
|
||||||
|
];
|
||||||
|
|
||||||
|
$nonLockablePreferences = [
|
||||||
|
'activeGroup',
|
||||||
|
'defaultGroup',
|
||||||
|
'useWebauthnOnly',
|
||||||
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -107,37 +149,17 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'preferences' => [
|
'preferences' => $preferences,
|
||||||
'showOtpAsDot' => false,
|
|
||||||
'showNextOtp' => false,
|
|
||||||
'revealDottedOTP' => false,
|
/*
|
||||||
'closeOtpOnCopy' => false,
|
|--------------------------------------------------------------------------
|
||||||
'copyOtpOnDisplay' => false,
|
| List of user preferences locked against user customization
|
||||||
'clearSearchOnCopy' => false,
|
| These settings cannot be overloaded and persisted by each user
|
||||||
'useBasicQrcodeReader' => false,
|
|--------------------------------------------------------------------------
|
||||||
'displayMode' => 'list',
|
|
|
||||||
'showAccountsIcons' => true,
|
*/
|
||||||
'kickUserAfter' => 15,
|
|
||||||
'activeGroup' => 0,
|
'lockedPreferences' => Helpers::lockedPreferences(Arr::except($preferences, $nonLockablePreferences)),
|
||||||
'rememberActiveGroup' => true,
|
|
||||||
'viewDefaultGroupOnCopy' => false,
|
|
||||||
'defaultGroup' => 0,
|
|
||||||
'defaultCaptureMode' => 'livescan',
|
|
||||||
'useDirectCapture' => false,
|
|
||||||
'useWebauthnOnly' => false,
|
|
||||||
'getOfficialIcons' => true,
|
|
||||||
'theme' => 'system',
|
|
||||||
'formatPassword' => true,
|
|
||||||
'formatPasswordBy' => 0.5,
|
|
||||||
'lang' => 'browser',
|
|
||||||
'getOtpOnRequest' => true,
|
|
||||||
'notifyOnNewAuthDevice' => false,
|
|
||||||
'notifyOnFailedLogin' => false,
|
|
||||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
|
||||||
'sortCaseSensitive' => false,
|
|
||||||
'autoCloseTimeout' => 2,
|
|
||||||
'AutoSaveQrcodedAccount' => false,
|
|
||||||
'showEmailInFooter' => true,
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -26,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
isIndented: Boolean,
|
isIndented: Boolean,
|
||||||
isDisabled: Boolean,
|
isDisabled: Boolean,
|
||||||
|
isLocked: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue'])
|
const emit = defineEmits(['update:modelValue'])
|
||||||
@ -49,12 +50,14 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="field is-flex">
|
<div class="field is-flex">
|
||||||
<div v-if="isIndented" class="mx-2 pr-1" :style="{ 'opacity': isDisabled ? '0.5' : '1' }">
|
<div v-if="isIndented" class="mx-2 pr-1" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
|
||||||
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
|
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="model" :disabled="isDisabled" :aria-describedby="help ? legendId : undefined" />
|
<input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="model" :disabled="isDisabled || isLocked" :aria-describedby="help ? legendId : undefined" />
|
||||||
<label tabindex="0" :for="fieldName" class="label" :class="labelClass" v-html="$t(label)" v-on:keypress.space.prevent="toggleModel" />
|
<label tabindex="0" :for="fieldName" class="label" :class="labelClass" v-on:keypress.space.prevent="toggleModel">
|
||||||
|
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
|
||||||
|
</label>
|
||||||
<p :id="legendId" class="help" v-html="$t(help)" v-if="help" />
|
<p :id="legendId" class="help" v-html="$t(help)" v-if="help" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
isIndented: Boolean,
|
isIndented: Boolean,
|
||||||
|
isLocked: Boolean,
|
||||||
leftIcon: '',
|
leftIcon: '',
|
||||||
rightIcon: '',
|
rightIcon: '',
|
||||||
idSuffix: {
|
idSuffix: {
|
||||||
@ -57,14 +58,16 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-3" :class="{ 'pt-3' : hasOffset, 'is-flex' : isIndented }">
|
<div class="mb-3" :class="{ 'pt-3' : hasOffset, 'is-flex' : isIndented }">
|
||||||
<div v-if="isIndented" class="mx-2 pr-1" :style="{ 'opacity': isDisabled ? '0.5' : '1' }">
|
<div v-if="isIndented" class="mx-2 pr-1" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
|
||||||
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
|
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" :class="{ 'is-flex-grow-5' : isIndented }">
|
<div class="field" :class="{ 'is-flex-grow-5' : isIndented }">
|
||||||
<label :for="inputId" class="label" :style="{ 'opacity': isDisabled ? '0.5' : '1' }" v-html="$t(label)"></label>
|
<label :for="inputId" class="label" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
|
||||||
|
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
|
||||||
|
</label>
|
||||||
<div class="control" :class="{ 'has-icons-left' : leftIcon, 'has-icons-right': rightIcon }">
|
<div class="control" :class="{ 'has-icons-left' : leftIcon, 'has-icons-right': rightIcon }">
|
||||||
<input
|
<input
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled || isLocked"
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
:type="inputType"
|
:type="inputType"
|
||||||
class="input"
|
class="input"
|
||||||
|
@ -54,14 +54,15 @@
|
|||||||
idSuffix: {
|
idSuffix: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
}
|
},
|
||||||
|
isLocked: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
|
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
|
||||||
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
|
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
|
||||||
const legendId = useIdGenerator('legend', props.fieldName).inputId
|
const legendId = useIdGenerator('legend', props.fieldName).inputId
|
||||||
|
|
||||||
const fieldIsLocked = ref(props.isDisabled || props.isEditMode)
|
const fieldIsLocked = ref(props.isDisabled || props.isEditMode || props.isLocked)
|
||||||
const hasBeenTrimmed = ref(false)
|
const hasBeenTrimmed = ref(false)
|
||||||
const componentKey = ref(0);
|
const componentKey = ref(0);
|
||||||
|
|
||||||
@ -96,7 +97,9 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<label :for="inputId" class="label" v-html="$t(label)" />
|
<label :for="inputId" class="label">
|
||||||
|
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
|
||||||
|
</label>
|
||||||
<div class="field has-addons mb-0" :class="{ 'pt-3' : hasOffset }">
|
<div class="field has-addons mb-0" :class="{ 'pt-3' : hasOffset }">
|
||||||
<div class="control" :class="{ 'is-expanded': isExpanded }">
|
<div class="control" :class="{ 'is-expanded': isExpanded }">
|
||||||
<input
|
<input
|
||||||
|
@ -45,6 +45,7 @@
|
|||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
isLocked: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
|
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
|
||||||
@ -84,10 +85,12 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="field" :class="{ 'pt-3' : hasOffset }">
|
<div class="field" :class="{ 'pt-3' : hasOffset }">
|
||||||
<label :for="inputId" class="label" v-html="$t(label)" />
|
<label :for="inputId" class="label">
|
||||||
|
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
|
||||||
|
</label>
|
||||||
<div class="control has-icons-right">
|
<div class="control has-icons-right">
|
||||||
<input
|
<input
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled || isLocked"
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
:type="currentType"
|
:type="currentType"
|
||||||
class="input"
|
class="input"
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
isIndented: Boolean,
|
isIndented: Boolean,
|
||||||
isDisabled: Boolean,
|
isDisabled: Boolean,
|
||||||
|
isLocked: Boolean,
|
||||||
idSuffix: {
|
idSuffix: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@ -37,18 +38,20 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="field is-flex">
|
<div class="field is-flex">
|
||||||
<div v-if="isIndented" class="mx-2 pr-1" :style="{ 'opacity': isDisabled ? '0.5' : '1' }">
|
<div v-if="isIndented" class="mx-2 pr-1" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
|
||||||
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
|
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label :for="inputId" class="label" v-html="$t(label)" :style="{ 'opacity': isDisabled ? '0.5' : '1' }"></label>
|
<label :for="inputId" class="label" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
|
||||||
|
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
|
||||||
|
</label>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<div class="select">
|
<div class="select">
|
||||||
<select
|
<select
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
v-model="selected"
|
v-model="selected"
|
||||||
v-on:change="$emit('update:modelValue', $event.target.value)"
|
v-on:change="$emit('update:modelValue', $event.target.value)"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled || isLocked"
|
||||||
:aria-describedby="help ? legendId : undefined"
|
:aria-describedby="help ? legendId : undefined"
|
||||||
:aria-invalid="fieldError != undefined"
|
:aria-invalid="fieldError != undefined"
|
||||||
:aria-errormessage="fieldError != undefined ? valErrorId : undefined"
|
:aria-errormessage="fieldError != undefined ? valErrorId : undefined"
|
||||||
|
@ -42,6 +42,7 @@
|
|||||||
default: null
|
default: null
|
||||||
},
|
},
|
||||||
isIndented: Boolean,
|
isIndented: Boolean,
|
||||||
|
isLocked: Boolean,
|
||||||
leftIcon: '',
|
leftIcon: '',
|
||||||
rightIcon: '',
|
rightIcon: '',
|
||||||
idSuffix: {
|
idSuffix: {
|
||||||
@ -57,14 +58,16 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-3" :class="{ 'pt-3' : hasOffset, 'is-flex' : isIndented }">
|
<div class="mb-3" :class="{ 'pt-3' : hasOffset, 'is-flex' : isIndented }">
|
||||||
<div v-if="isIndented" class="mx-2 pr-1" :style="{ 'opacity': isDisabled ? '0.5' : '1' }">
|
<div v-if="isIndented" class="mx-2 pr-1" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
|
||||||
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
|
<FontAwesomeIcon class="has-text-grey" :icon="['fas', 'chevron-right']" transform="rotate-135"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="field" :class="{ 'is-flex-grow-5' : isIndented }">
|
<div class="field" :class="{ 'is-flex-grow-5' : isIndented }">
|
||||||
<label v-if="label" :for="inputId" class="label" v-html="$t(label)"></label>
|
<label v-if="label" :for="inputId" class="label">
|
||||||
|
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
|
||||||
|
</label>
|
||||||
<div class="control" :class="{ 'has-icons-left' : leftIcon, 'has-icons-right': rightIcon }">
|
<div class="control" :class="{ 'has-icons-left' : leftIcon, 'has-icons-right': rightIcon }">
|
||||||
<textarea
|
<textarea
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled || isLocked"
|
||||||
:id="inputId"
|
:id="inputId"
|
||||||
class="textarea"
|
class="textarea"
|
||||||
:class="size"
|
:class="size"
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
fieldError: [String],
|
fieldError: [String],
|
||||||
hasOffset: Boolean,
|
hasOffset: Boolean,
|
||||||
isDisabled: Boolean,
|
isDisabled: Boolean,
|
||||||
|
isLocked: Boolean,
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: ''
|
default: ''
|
||||||
@ -38,7 +39,9 @@
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="field" :class="{ 'pt-3': hasOffset }">
|
<div class="field" :class="{ 'pt-3': hasOffset }">
|
||||||
<span v-if="label" class="label" v-html="$t(label)" />
|
<span v-if="label" class="label" :class="{ 'is-opacity-5' : isDisabled || isLocked }">
|
||||||
|
{{ $t(label) }}<FontAwesomeIcon v-if="isLocked" :icon="['fas', 'lock']" class="ml-2" size="xs" />
|
||||||
|
</span>
|
||||||
<div
|
<div
|
||||||
id="rdoGroup"
|
id="rdoGroup"
|
||||||
role="radiogroup"
|
role="radiogroup"
|
||||||
@ -56,7 +59,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="button"
|
class="button"
|
||||||
:aria-checked="modelValue===choice.value"
|
:aria-checked="modelValue===choice.value"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled || isLocked"
|
||||||
:class="{
|
:class="{
|
||||||
'is-link': modelValue===choice.value,
|
'is-link': modelValue===choice.value,
|
||||||
'is-dark': mode==='dark',
|
'is-dark': mode==='dark',
|
||||||
@ -69,7 +72,7 @@
|
|||||||
class="is-hidden"
|
class="is-hidden"
|
||||||
:checked="modelValue===choice.value"
|
:checked="modelValue===choice.value"
|
||||||
:value="choice.value"
|
:value="choice.value"
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled || isLocked"
|
||||||
/>
|
/>
|
||||||
<span v-if="choice.legend" v-html="$t(choice.legend)" class="is-block is-size-7" />
|
<span v-if="choice.legend" v-html="$t(choice.legend)" class="is-block is-size-7" />
|
||||||
<FontAwesomeIcon :icon="['fas',choice.icon]" v-if="choice.icon" class="mr-2" />
|
<FontAwesomeIcon :icon="['fas',choice.icon]" v-if="choice.icon" class="mr-2" />
|
||||||
|
5
resources/js/stores/appSettings.js
vendored
5
resources/js/stores/appSettings.js
vendored
@ -6,7 +6,10 @@ export const useAppSettingsStore = defineStore({
|
|||||||
id: 'appSettings',
|
id: 'appSettings',
|
||||||
|
|
||||||
state: () => {
|
state: () => {
|
||||||
return { ...window.appSettings }
|
return {
|
||||||
|
lockedPreferences: window.lockedPreferences,
|
||||||
|
...window.appSettings
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getters: {
|
getters: {
|
||||||
|
10
resources/js/stores/user.js
vendored
10
resources/js/stores/user.js
vendored
@ -137,10 +137,20 @@ export const useUserStore = defineStore({
|
|||||||
* Refresh user preferences with backend state
|
* Refresh user preferences with backend state
|
||||||
*/
|
*/
|
||||||
refreshPreferences() {
|
refreshPreferences() {
|
||||||
|
const appSettings = useAppSettingsStore()
|
||||||
|
|
||||||
userService.getPreferences({returnError: true})
|
userService.getPreferences({returnError: true})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
response.data.forEach(preference => {
|
response.data.forEach(preference => {
|
||||||
this.preferences[preference.key] = preference.value
|
this.preferences[preference.key] = preference.value
|
||||||
|
let index = appSettings.lockedPreferences.indexOf(preference.key)
|
||||||
|
|
||||||
|
if (preference.locked == true && index === -1) {
|
||||||
|
appSettings.lockedPreferences.push(preference.key)
|
||||||
|
}
|
||||||
|
else if (preference.locked == false && index > 0) {
|
||||||
|
appSettings.lockedPreferences.splice(index, 1)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
@ -4,12 +4,14 @@
|
|||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import { useGroups } from '@/stores/groups'
|
import { useGroups } from '@/stores/groups'
|
||||||
import { useNotifyStore } from '@/stores/notify'
|
import { useNotifyStore } from '@/stores/notify'
|
||||||
|
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||||
import { timezones } from './timezones'
|
import { timezones } from './timezones'
|
||||||
|
|
||||||
const $2fauth = inject('2fauth')
|
const $2fauth = inject('2fauth')
|
||||||
const user = useUserStore()
|
const user = useUserStore()
|
||||||
const groups = useGroups()
|
const groups = useGroups()
|
||||||
const notify = useNotifyStore()
|
const notify = useNotifyStore()
|
||||||
|
const appSettings = useAppSettingsStore()
|
||||||
const returnTo = useStorage($2fauth.prefix + 'returnTo', 'accounts')
|
const returnTo = useStorage($2fauth.prefix + 'returnTo', 'accounts')
|
||||||
|
|
||||||
const layouts = [
|
const layouts = [
|
||||||
@ -121,8 +123,11 @@
|
|||||||
<!-- user preferences -->
|
<!-- user preferences -->
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
|
<h4 class="title is-4 has-text-grey-light">{{ $t('settings.general') }}</h4>
|
||||||
|
<div v-if="appSettings.lockedPreferences.length > 0" class="notification is-warning">
|
||||||
|
{{ $t('settings.settings_managed_by_administrator') }}
|
||||||
|
</div>
|
||||||
<!-- Language -->
|
<!-- Language -->
|
||||||
<FormSelect v-model="user.preferences.lang" @update:model-value="val => savePreference('lang', val)" :options="langs" fieldName="lang" label="settings.forms.language.label" help="settings.forms.language.help" />
|
<FormSelect v-model="user.preferences.lang" @update:model-value="val => savePreference('lang', val)" :options="langs" fieldName="lang" :isLocked="appSettings.lockedPreferences.includes('lang')" label="settings.forms.language.label" help="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">
|
||||||
@ -131,66 +136,66 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- timezone -->
|
<!-- timezone -->
|
||||||
<FormSelect v-model="user.preferences.timezone" @update:model-value="val => savePreference('timezone', val)" :options="timezones" fieldName="timezone" label="settings.forms.timezone.label" help="settings.forms.timezone.help" />
|
<FormSelect v-model="user.preferences.timezone" @update:model-value="val => savePreference('timezone', val)" :options="timezones" fieldName="timezone" :isLocked="appSettings.lockedPreferences.includes('timezone')" label="settings.forms.timezone.label" help="settings.forms.timezone.help" />
|
||||||
<!-- display mode -->
|
<!-- display mode -->
|
||||||
<FormToggle v-model="user.preferences.displayMode" @update:model-value="val => savePreference('displayMode', val)" :choices="layouts" fieldName="displayMode" label="settings.forms.display_mode.label" help="settings.forms.display_mode.help"/>
|
<FormToggle v-model="user.preferences.displayMode" @update:model-value="val => savePreference('displayMode', val)" :choices="layouts" fieldName="displayMode" :isLocked="appSettings.lockedPreferences.includes('displayMode')" label="settings.forms.display_mode.label" help="settings.forms.display_mode.help" />
|
||||||
<!-- theme -->
|
<!-- theme -->
|
||||||
<FormToggle v-model="user.preferences.theme" @update:model-value="val => savePreference('theme', val)" :choices="themes" fieldName="theme" label="settings.forms.theme.label" help="settings.forms.theme.help"/>
|
<FormToggle v-model="user.preferences.theme" @update:model-value="val => savePreference('theme', val)" :choices="themes" fieldName="theme" :isLocked="appSettings.lockedPreferences.includes('theme')" label="settings.forms.theme.label" help="settings.forms.theme.help" />
|
||||||
<!-- show icon -->
|
<!-- show icon -->
|
||||||
<FormCheckbox v-model="user.preferences.showAccountsIcons" @update:model-value="val => savePreference('showAccountsIcons', val)" fieldName="showAccountsIcons" label="settings.forms.show_accounts_icons.label" help="settings.forms.show_accounts_icons.help" />
|
<FormCheckbox v-model="user.preferences.showAccountsIcons" @update:model-value="val => savePreference('showAccountsIcons', val)" fieldName="showAccountsIcons" :isLocked="appSettings.lockedPreferences.includes('showAccountsIcons')" label="settings.forms.show_accounts_icons.label" help="settings.forms.show_accounts_icons.help" />
|
||||||
<!-- Official icons -->
|
<!-- Official icons -->
|
||||||
<FormCheckbox v-model="user.preferences.getOfficialIcons" @update:model-value="val => savePreference('getOfficialIcons', val)" fieldName="getOfficialIcons" label="settings.forms.get_official_icons.label" help="settings.forms.get_official_icons.help" />
|
<FormCheckbox v-model="user.preferences.getOfficialIcons" @update:model-value="val => savePreference('getOfficialIcons', val)" fieldName="getOfficialIcons" :isLocked="appSettings.lockedPreferences.includes('getOfficialIcons')" label="settings.forms.get_official_icons.label" help="settings.forms.get_official_icons.help" />
|
||||||
<!-- password format -->
|
<!-- password format -->
|
||||||
<FormCheckbox v-model="user.preferences.formatPassword" @update:model-value="val => savePreference('formatPassword', val)" fieldName="formatPassword" label="settings.forms.password_format.label" help="settings.forms.password_format.help" />
|
<FormCheckbox v-model="user.preferences.formatPassword" @update:model-value="val => savePreference('formatPassword', val)" fieldName="formatPassword" :isLocked="appSettings.lockedPreferences.includes('formatPassword')" label="settings.forms.password_format.label" help="settings.forms.password_format.help" />
|
||||||
<FormToggle v-model="user.preferences.formatPasswordBy" @update:model-value="val => savePreference('formatPasswordBy', val)" :choices="passwordFormats" fieldName="formatPasswordBy" :isDisabled="!user.preferences.formatPassword" />
|
<FormToggle v-model="user.preferences.formatPasswordBy" @update:model-value="val => savePreference('formatPasswordBy', val)" :choices="passwordFormats" fieldName="formatPasswordBy" :isLocked="appSettings.lockedPreferences.includes('formatPasswordBy')" :isDisabled="!user.preferences.formatPassword" />
|
||||||
<!-- clear search on copy -->
|
<!-- clear search on copy -->
|
||||||
<FormCheckbox v-model="user.preferences.clearSearchOnCopy" @update:model-value="val => savePreference('clearSearchOnCopy', val)" fieldName="clearSearchOnCopy" label="settings.forms.clear_search_on_copy.label" help="settings.forms.clear_search_on_copy.help" />
|
<FormCheckbox v-model="user.preferences.clearSearchOnCopy" @update:model-value="val => savePreference('clearSearchOnCopy', val)" fieldName="clearSearchOnCopy" :isLocked="appSettings.lockedPreferences.includes('clearSearchOnCopy')" label="settings.forms.clear_search_on_copy.label" help="settings.forms.clear_search_on_copy.help" />
|
||||||
<!-- sort case sensitive -->
|
<!-- sort case sensitive -->
|
||||||
<FormCheckbox v-model="user.preferences.sortCaseSensitive" @update:model-value="val => savePreference('sortCaseSensitive', val)" fieldName="sortCaseSensitive" label="settings.forms.sort_case_sensitive.label" help="settings.forms.sort_case_sensitive.help" />
|
<FormCheckbox v-model="user.preferences.sortCaseSensitive" @update:model-value="val => savePreference('sortCaseSensitive', val)" fieldName="sortCaseSensitive" :isLocked="appSettings.lockedPreferences.includes('sortCaseSensitive')" label="settings.forms.sort_case_sensitive.label" help="settings.forms.sort_case_sensitive.help" />
|
||||||
<!-- show email in footer -->
|
<!-- show email in footer -->
|
||||||
<FormCheckbox v-model="user.preferences.showEmailInFooter" @update:model-value="val => savePreference('showEmailInFooter', val)" fieldName="showEmailInFooter" label="settings.forms.show_email_in_footer.label" help="settings.forms.show_email_in_footer.help" />
|
<FormCheckbox v-model="user.preferences.showEmailInFooter" @update:model-value="val => savePreference('showEmailInFooter', val)" fieldName="showEmailInFooter" :isLocked="appSettings.lockedPreferences.includes('showEmailInFooter')" label="settings.forms.show_email_in_footer.label" help="settings.forms.show_email_in_footer.help" />
|
||||||
|
|
||||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('groups.groups') }}</h4>
|
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('groups.groups') }}</h4>
|
||||||
<!-- default group -->
|
<!-- default group -->
|
||||||
<FormSelect v-model="user.preferences.defaultGroup" @update:model-value="val => savePreference('defaultGroup', val)" :options="groupsList" fieldName="defaultGroup" label="settings.forms.default_group.label" help="settings.forms.default_group.help" />
|
<FormSelect v-model="user.preferences.defaultGroup" @update:model-value="val => savePreference('defaultGroup', val)" :options="groupsList" fieldName="defaultGroup" label="settings.forms.default_group.label" help="settings.forms.default_group.help" />
|
||||||
<!-- retain active group -->
|
<!-- retain active group -->
|
||||||
<FormCheckbox v-model="user.preferences.rememberActiveGroup" @update:model-value="val => savePreference('rememberActiveGroup', val)" fieldName="rememberActiveGroup" label="settings.forms.remember_active_group.label" help="settings.forms.remember_active_group.help" />
|
<FormCheckbox v-model="user.preferences.rememberActiveGroup" @update:model-value="val => savePreference('rememberActiveGroup', val)" fieldName="rememberActiveGroup" :isLocked="appSettings.lockedPreferences.includes('rememberActiveGroup')" label="settings.forms.remember_active_group.label" help="settings.forms.remember_active_group.help" />
|
||||||
<!-- always return to default group after copying -->
|
<!-- always return to default group after copying -->
|
||||||
<FormCheckbox v-model="user.preferences.viewDefaultGroupOnCopy" @update:model-value="val => savePreference('viewDefaultGroupOnCopy', val)" fieldName="viewDefaultGroupOnCopy" label="settings.forms.view_default_group_on_copy.label" help="settings.forms.view_default_group_on_copy.help" />
|
<FormCheckbox v-model="user.preferences.viewDefaultGroupOnCopy" @update:model-value="val => savePreference('viewDefaultGroupOnCopy', val)" fieldName="viewDefaultGroupOnCopy" :isLocked="appSettings.lockedPreferences.includes('viewDefaultGroupOnCopy')" label="settings.forms.view_default_group_on_copy.label" help="settings.forms.view_default_group_on_copy.help" />
|
||||||
|
|
||||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
|
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.security') }}</h4>
|
||||||
<!-- auto lock -->
|
<!-- auto lock -->
|
||||||
<FormSelect v-model="user.preferences.kickUserAfter" @update:model-value="val => savePreference('kickUserAfter', val)" :options="kickUserAfters" fieldName="kickUserAfter" label="settings.forms.auto_lock.label" help="settings.forms.auto_lock.help" />
|
<FormSelect v-model="user.preferences.kickUserAfter" @update:model-value="val => savePreference('kickUserAfter', val)" :options="kickUserAfters" fieldName="kickUserAfter" :isLocked="appSettings.lockedPreferences.includes('kickUserAfter')" label="settings.forms.auto_lock.label" help="settings.forms.auto_lock.help" />
|
||||||
<!-- get OTP on request -->
|
<!-- get OTP on request -->
|
||||||
<FormToggle v-model="user.preferences.getOtpOnRequest" @update:model-value="val => savePreference('getOtpOnRequest', val)" :choices="getOtpTriggers" fieldName="getOtpOnRequest" label="settings.forms.otp_generation.label" help="settings.forms.otp_generation.help"/>
|
<FormToggle v-model="user.preferences.getOtpOnRequest" @update:model-value="val => savePreference('getOtpOnRequest', val)" :choices="getOtpTriggers" fieldName="getOtpOnRequest" :isLocked="appSettings.lockedPreferences.includes('getOtpOnRequest')" label="settings.forms.otp_generation.label" help="settings.forms.otp_generation.help"/>
|
||||||
<!-- close otp on copy -->
|
<!-- close otp on copy -->
|
||||||
<FormCheckbox v-model="user.preferences.closeOtpOnCopy" @update:model-value="val => savePreference('closeOtpOnCopy', val)" fieldName="closeOtpOnCopy" label="settings.forms.close_otp_on_copy.label" help="settings.forms.close_otp_on_copy.help" :isDisabled="!user.preferences.getOtpOnRequest" :isIndented="true" />
|
<FormCheckbox v-model="user.preferences.closeOtpOnCopy" @update:model-value="val => savePreference('closeOtpOnCopy', val)" fieldName="closeOtpOnCopy" :isLocked="appSettings.lockedPreferences.includes('closeOtpOnCopy')" :isDisabled="!user.preferences.getOtpOnRequest" label="settings.forms.close_otp_on_copy.label" help="settings.forms.close_otp_on_copy.help" :isIndented="true" />
|
||||||
<!-- auto-close timeout -->
|
<!-- auto-close timeout -->
|
||||||
<FormSelect v-model="user.preferences.autoCloseTimeout" @update:model-value="val => savePreference('autoCloseTimeout', val)" :options="autoCloseTimeout" fieldName="autoCloseTimeout" label="settings.forms.auto_close_timeout.label" help="settings.forms.auto_close_timeout.help" :isDisabled="!user.preferences.getOtpOnRequest" :isIndented="true" />
|
<FormSelect v-model="user.preferences.autoCloseTimeout" @update:model-value="val => savePreference('autoCloseTimeout', val)" :options="autoCloseTimeout" fieldName="autoCloseTimeout" :isLocked="appSettings.lockedPreferences.includes('autoCloseTimeout')" :isDisabled="!user.preferences.getOtpOnRequest" label="settings.forms.auto_close_timeout.label" help="settings.forms.auto_close_timeout.help" :isIndented="true" />
|
||||||
<!-- clear search on copy -->
|
<!-- clear search on copy -->
|
||||||
<FormCheckbox v-model="user.preferences.copyOtpOnDisplay" @update:model-value="val => savePreference('copyOtpOnDisplay', val)" fieldName="copyOtpOnDisplay" label="settings.forms.copy_otp_on_display.label" help="settings.forms.copy_otp_on_display.help" :isDisabled="!user.preferences.getOtpOnRequest" :isIndented="true" />
|
<FormCheckbox v-model="user.preferences.copyOtpOnDisplay" @update:model-value="val => savePreference('copyOtpOnDisplay', val)" fieldName="copyOtpOnDisplay" :isLocked="appSettings.lockedPreferences.includes('copyOtpOnDisplay')" :isDisabled="!user.preferences.getOtpOnRequest" label="settings.forms.copy_otp_on_display.label" help="settings.forms.copy_otp_on_display.help" :isIndented="true" />
|
||||||
<!-- otp as dot -->
|
<!-- otp as dot -->
|
||||||
<FormCheckbox v-model="user.preferences.showOtpAsDot" @update:model-value="val => savePreference('showOtpAsDot', val)" fieldName="showOtpAsDot" label="settings.forms.show_otp_as_dot.label" help="settings.forms.show_otp_as_dot.help" />
|
<FormCheckbox v-model="user.preferences.showOtpAsDot" @update:model-value="val => savePreference('showOtpAsDot', val)" fieldName="showOtpAsDot" :isLocked="appSettings.lockedPreferences.includes('showOtpAsDot')" label="settings.forms.show_otp_as_dot.label" help="settings.forms.show_otp_as_dot.help" />
|
||||||
<!-- reveal dotted OTPs -->
|
<!-- reveal dotted OTPs -->
|
||||||
<FormCheckbox v-model="user.preferences.revealDottedOTP" @update:model-value="val => savePreference('revealDottedOTP', val)" fieldName="revealDottedOTP" label="settings.forms.reveal_dotted_otp.label" help="settings.forms.reveal_dotted_otp.help" :isDisabled="!user.preferences.showOtpAsDot" :isIndented="true" />
|
<FormCheckbox v-model="user.preferences.revealDottedOTP" @update:model-value="val => savePreference('revealDottedOTP', val)" fieldName="revealDottedOTP" :isLocked="appSettings.lockedPreferences.includes('revealDottedOTP')" :isDisabled="!user.preferences.showOtpAsDot" label="settings.forms.reveal_dotted_otp.label" help="settings.forms.reveal_dotted_otp.help" :isIndented="true" />
|
||||||
<!-- show next OTP -->
|
<!-- show next OTP -->
|
||||||
<FormCheckbox v-model="user.preferences.showNextOtp" @update:model-value="val => savePreference('showNextOtp', val)" fieldName="showNextOtp" label="settings.forms.show_next_otp.label" help="settings.forms.show_next_otp.help" />
|
<FormCheckbox v-model="user.preferences.showNextOtp" @update:model-value="val => savePreference('showNextOtp', val)" fieldName="showNextOtp" :isLocked="appSettings.lockedPreferences.includes('showNextOtp')" label="settings.forms.show_next_otp.label" help="settings.forms.show_next_otp.help" />
|
||||||
|
|
||||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.notifications') }}</h4>
|
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.notifications') }}</h4>
|
||||||
<!-- on new device -->
|
<!-- on new device -->
|
||||||
<FormCheckbox v-model="user.preferences.notifyOnNewAuthDevice" @update:model-value="val => savePreference('notifyOnNewAuthDevice', val)" fieldName="notifyOnNewAuthDevice" label="settings.forms.notify_on_new_auth_device.label" help="settings.forms.notify_on_new_auth_device.help" />
|
<FormCheckbox v-model="user.preferences.notifyOnNewAuthDevice" @update:model-value="val => savePreference('notifyOnNewAuthDevice', val)" fieldName="notifyOnNewAuthDevice" :isLocked="appSettings.lockedPreferences.includes('notifyOnNewAuthDevice')" label="settings.forms.notify_on_new_auth_device.label" help="settings.forms.notify_on_new_auth_device.help" />
|
||||||
<!-- on failed login -->
|
<!-- on failed login -->
|
||||||
<FormCheckbox v-model="user.preferences.notifyOnFailedLogin" @update:model-value="val => savePreference('notifyOnFailedLogin', val)" fieldName="notifyOnFailedLogin" label="settings.forms.notify_on_failed_login.label" help="settings.forms.notify_on_failed_login.help" />
|
<FormCheckbox v-model="user.preferences.notifyOnFailedLogin" @update:model-value="val => savePreference('notifyOnFailedLogin', val)" fieldName="notifyOnFailedLogin" :isLocked="appSettings.lockedPreferences.includes('notifyOnFailedLogin')" label="settings.forms.notify_on_failed_login.label" help="settings.forms.notify_on_failed_login.help" />
|
||||||
|
|
||||||
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
|
<h4 class="title is-4 pt-4 has-text-grey-light">{{ $t('settings.data_input') }}</h4>
|
||||||
<!-- auto-save QrCoded account -->
|
<!-- auto-save QrCoded account -->
|
||||||
<FormCheckbox v-model="user.preferences.AutoSaveQrcodedAccount" @update:model-value="val => savePreference('AutoSaveQrcodedAccount', val)" fieldName="AutoSaveQrcodedAccount" label="settings.forms.auto_save_qrcoded_account.label" help="settings.forms.auto_save_qrcoded_account.help" />
|
<FormCheckbox v-model="user.preferences.AutoSaveQrcodedAccount" @update:model-value="val => savePreference('AutoSaveQrcodedAccount', val)" fieldName="AutoSaveQrcodedAccount" :isLocked="appSettings.lockedPreferences.includes('AutoSaveQrcodedAccount')" label="settings.forms.auto_save_qrcoded_account.label" help="settings.forms.auto_save_qrcoded_account.help" />
|
||||||
<!-- basic qrcode -->
|
<!-- basic qrcode -->
|
||||||
<FormCheckbox v-model="user.preferences.useBasicQrcodeReader" @update:model-value="val => savePreference('useBasicQrcodeReader', val)" fieldName="useBasicQrcodeReader" label="settings.forms.use_basic_qrcode_reader.label" help="settings.forms.use_basic_qrcode_reader.help" />
|
<FormCheckbox v-model="user.preferences.useBasicQrcodeReader" @update:model-value="val => savePreference('useBasicQrcodeReader', val)" fieldName="useBasicQrcodeReader" :isLocked="appSettings.lockedPreferences.includes('useBasicQrcodeReader')" label="settings.forms.use_basic_qrcode_reader.label" help="settings.forms.use_basic_qrcode_reader.help" />
|
||||||
<!-- direct capture -->
|
<!-- direct capture -->
|
||||||
<FormCheckbox v-model="user.preferences.useDirectCapture" @update:model-value="val => savePreference('useDirectCapture', val)" fieldName="useDirectCapture" label="settings.forms.useDirectCapture.label" help="settings.forms.useDirectCapture.help" />
|
<FormCheckbox v-model="user.preferences.useDirectCapture" @update:model-value="val => savePreference('useDirectCapture', val)" fieldName="useDirectCapture" :isLocked="appSettings.lockedPreferences.includes('useDirectCapture')" label="settings.forms.useDirectCapture.label" help="settings.forms.useDirectCapture.help" />
|
||||||
<!-- default capture mode -->
|
<!-- default capture mode -->
|
||||||
<FormSelect v-model="user.preferences.defaultCaptureMode" @update:model-value="val => savePreference('defaultCaptureMode', val)" :options="captureModes" fieldName="defaultCaptureMode" label="settings.forms.defaultCaptureMode.label" help="settings.forms.defaultCaptureMode.help" :isDisabled="!user.preferences.useDirectCapture" :isIndented="true" />
|
<FormSelect v-model="user.preferences.defaultCaptureMode" @update:model-value="val => savePreference('defaultCaptureMode', val)" :options="captureModes" fieldName="defaultCaptureMode" :isLocked="appSettings.lockedPreferences.includes('defaultCaptureMode')" :isDisabled="!user.preferences.useDirectCapture" label="settings.forms.defaultCaptureMode.label" help="settings.forms.defaultCaptureMode.help" :isIndented="true" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</FormWrapper>
|
</FormWrapper>
|
||||||
|
@ -43,6 +43,7 @@ return [
|
|||||||
],
|
],
|
||||||
'make_sure_copy_token' => 'Make sure to copy your personal access token now. You won’t be able to see it again!',
|
'make_sure_copy_token' => 'Make sure to copy your personal access token now. You won’t be able to see it again!',
|
||||||
'data_input' => 'Data input',
|
'data_input' => 'Data input',
|
||||||
|
'settings_managed_by_administrator' => 'Some settings are being managed by your administrator',
|
||||||
'forms' => [
|
'forms' => [
|
||||||
'edit_settings' => 'Edit settings',
|
'edit_settings' => 'Edit settings',
|
||||||
'setting_saved' => 'Settings saved',
|
'setting_saved' => 'Settings saved',
|
||||||
@ -58,23 +59,23 @@ return [
|
|||||||
'help' => 'The time zone applied to all dates and times displayed in the application'
|
'help' => 'The time zone applied to all dates and times displayed in the application'
|
||||||
],
|
],
|
||||||
'show_otp_as_dot' => [
|
'show_otp_as_dot' => [
|
||||||
'label' => 'Show generated <abbr title="One-Time Password">OTP</abbr> as dot',
|
'label' => 'Show generated OTP as dot',
|
||||||
'help' => 'Replace generated password characters with *** to ensure confidentiality. Does not affect the copy/paste feature'
|
'help' => 'Replace generated password characters with *** to ensure confidentiality. Does not affect the copy/paste feature'
|
||||||
],
|
],
|
||||||
'reveal_dotted_otp' => [
|
'reveal_dotted_otp' => [
|
||||||
'label' => 'Reveal obscured <abbr title="One-Time Password">OTP</abbr>',
|
'label' => 'Reveal obscured OTP',
|
||||||
'help' => 'Let the ability to temporarily reveal Dot-Obscured passwords'
|
'help' => 'Let the ability to temporarily reveal Dot-Obscured passwords'
|
||||||
],
|
],
|
||||||
'close_otp_on_copy' => [
|
'close_otp_on_copy' => [
|
||||||
'label' => 'Close <abbr title="One-Time Password">OTP</abbr> after copy',
|
'label' => 'Close OTP after copy',
|
||||||
'help' => 'Click on a generated password to copy it automatically hides it from the screen'
|
'help' => 'Click on a generated password to copy it automatically hides it from the screen'
|
||||||
],
|
],
|
||||||
'show_next_otp' => [
|
'show_next_otp' => [
|
||||||
'label' => 'Show next <abbr title="One-Time Password">OTP</abbr>',
|
'label' => 'Show next OTP',
|
||||||
'help' => 'Preview the next password, i.e. the password that will replace the current password when it expires. Preferences set for the current OTP also apply to the next one (formatting, show as dot)'
|
'help' => 'Preview the next password, i.e. the password that will replace the current password when it expires. Preferences set for the current OTP also apply to the next one (formatting, show as dot)'
|
||||||
],
|
],
|
||||||
'auto_close_timeout' => [
|
'auto_close_timeout' => [
|
||||||
'label' => 'Auto close <abbr title="One-Time Password">OTP</abbr>',
|
'label' => 'Auto close OTP',
|
||||||
'help' => 'Automatically hide on-screen password after a timeout. This avoids unnecessary requests for fresh passwords if you forget to close the password view.'
|
'help' => 'Automatically hide on-screen password after a timeout. This avoids unnecessary requests for fresh passwords if you forget to close the password view.'
|
||||||
],
|
],
|
||||||
'clear_search_on_copy' => [
|
'clear_search_on_copy' => [
|
||||||
@ -86,7 +87,7 @@ return [
|
|||||||
'help' => 'When invoked, force the Sort function to sort accounts on a case-sensitive basis'
|
'help' => 'When invoked, force the Sort function to sort accounts on a case-sensitive basis'
|
||||||
],
|
],
|
||||||
'copy_otp_on_display' => [
|
'copy_otp_on_display' => [
|
||||||
'label' => 'Copy <abbr title="One-Time Password">OTP</abbr> on display',
|
'label' => 'Copy OTP on display',
|
||||||
'help' => 'Automatically copy a generated password right after it appears on screen. Due to browsers limitations, only the first <abbr title="Time-based One-Time Password">TOTP</abbr> password will be copied, not the rotating ones'
|
'help' => 'Automatically copy a generated password right after it appears on screen. Due to browsers limitations, only the first <abbr title="Time-based One-Time Password">TOTP</abbr> password will be copied, not the rotating ones'
|
||||||
],
|
],
|
||||||
'use_basic_qrcode_reader' => [
|
'use_basic_qrcode_reader' => [
|
||||||
|
@ -78,7 +78,7 @@ return [
|
|||||||
],
|
],
|
||||||
'plain_text' => 'Plain text',
|
'plain_text' => 'Plain text',
|
||||||
'otp_type' => [
|
'otp_type' => [
|
||||||
'label' => 'Choose the type of <abbr title="One-Time Password">OTP</abbr> to create',
|
'label' => 'Choose the type of OTP to create',
|
||||||
'help' => 'Time-based OTP or HMAC-based OTP or Steam OTP'
|
'help' => 'Time-based OTP or HMAC-based OTP or Steam OTP'
|
||||||
],
|
],
|
||||||
'digits' => [
|
'digits' => [
|
||||||
|
1
resources/views/landing.blade.php
vendored
1
resources/views/landing.blade.php
vendored
@ -27,6 +27,7 @@
|
|||||||
var appConfig = {!! $appConfig !!};
|
var appConfig = {!! $appConfig !!};
|
||||||
var urls = {!! $urls !!};
|
var urls = {!! $urls !!};
|
||||||
var defaultPreferences = {!! $defaultPreferences->toJson() !!};
|
var defaultPreferences = {!! $defaultPreferences->toJson() !!};
|
||||||
|
var lockedPreferences = {!! $lockedPreferences->toJson() !!};
|
||||||
var appVersion = '{{ config("2fauth.version") }}';
|
var appVersion = '{{ config("2fauth.version") }}';
|
||||||
var isDemoApp = {!! $isDemoApp !!};
|
var isDemoApp = {!! $isDemoApp !!};
|
||||||
var isTestingApp = {!! $isTestingApp !!};
|
var isTestingApp = {!! $isTestingApp !!};
|
||||||
|
@ -24,6 +24,7 @@ class UserControllerTest extends FeatureTestCase
|
|||||||
private const PREFERENCE_JSON_STRUCTURE = [
|
private const PREFERENCE_JSON_STRUCTURE = [
|
||||||
'key',
|
'key',
|
||||||
'value',
|
'value',
|
||||||
|
'locked',
|
||||||
];
|
];
|
||||||
|
|
||||||
public function setUp() : void
|
public function setUp() : void
|
||||||
@ -62,7 +63,7 @@ class UserControllerTest extends FeatureTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_allPreferences_returns_preferences_with_default_values()
|
public function test_allPreferences_returns_preferences_with_default_config_values()
|
||||||
{
|
{
|
||||||
$response = $this->actingAs($this->user, 'api-guard')
|
$response = $this->actingAs($this->user, 'api-guard')
|
||||||
->json('GET', '/api/v1/user/preferences')
|
->json('GET', '/api/v1/user/preferences')
|
||||||
@ -108,7 +109,7 @@ class UserControllerTest extends FeatureTestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function test_showPreference_returns_preference_with_default_value()
|
public function test_showPreference_returns_preference_with_default_config_value()
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||||
@ -121,6 +122,28 @@ class UserControllerTest extends FeatureTestCase
|
|||||||
->assertExactJson([
|
->assertExactJson([
|
||||||
'key' => 'showOtpAsDot',
|
'key' => 'showOtpAsDot',
|
||||||
'value' => config('2fauth.preferences.showOtpAsDot'),
|
'value' => config('2fauth.preferences.showOtpAsDot'),
|
||||||
|
'locked' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_showPreference_returns_preference_with_locked_default_env_value()
|
||||||
|
{
|
||||||
|
// See .env.testing which sets USERPREF_DEFAULT__THEME=light
|
||||||
|
// while config/2fauth.php sets the default value to 'system'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \App\Models\User|\Illuminate\Contracts\Auth\Authenticatable
|
||||||
|
*/
|
||||||
|
$this->user = User::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($this->user, 'api-guard')
|
||||||
|
->json('GET', '/api/v1/user/preferences/theme')
|
||||||
|
->assertOk()
|
||||||
|
->assertExactJson([
|
||||||
|
'key' => 'theme',
|
||||||
|
'value' => 'light',
|
||||||
|
'locked' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,4 +213,16 @@ class UserControllerTest extends FeatureTestCase
|
|||||||
])
|
])
|
||||||
->assertStatus(422);
|
->assertStatus(422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_setPreference_on_locked_preference_returns_forbidden()
|
||||||
|
{
|
||||||
|
// See .env.testing which sets USERPREF_LOCKED__THEME=true
|
||||||
|
$response = $this->actingAs($this->user, 'api-guard')
|
||||||
|
->json('PUT', '/api/v1/user/preferences/theme', [
|
||||||
|
'key' => 'theme',
|
||||||
|
'value' => 'system',
|
||||||
|
])
|
||||||
|
->assertStatus(403);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,4 +218,42 @@ class HelpersTest extends TestCase
|
|||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lockedPreferences_returns_locked_preferences()
|
||||||
|
{
|
||||||
|
// See .env.testing which sets USERPREF_DEFAULT__THEME=light
|
||||||
|
// while config/2fauth.php sets the default value to 'system'
|
||||||
|
$lockedPreferences = Helpers::lockedPreferences(config('2fauth.preferences'));
|
||||||
|
|
||||||
|
$this->assertContains('theme', $lockedPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lockedPreferences_returns_empty_array_when_empty_array_is_provided()
|
||||||
|
{
|
||||||
|
$param = [];
|
||||||
|
|
||||||
|
$lockedPreferences = Helpers::lockedPreferences($param);
|
||||||
|
|
||||||
|
$this->assertEquals([], $lockedPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lockedPreferences_excludes_preference_when_env_var_is_empty()
|
||||||
|
{
|
||||||
|
// See .env.testing which sets USERPREF_LOCKED__DISPLAY_MODE=
|
||||||
|
$lockedPreferences = Helpers::lockedPreferences(config('2fauth.preferences'));
|
||||||
|
|
||||||
|
$this->assertNotContains('displayMode', $lockedPreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Test]
|
||||||
|
public function test_lockedPreferences_excludes_preference_when_env_var_is_malformed()
|
||||||
|
{
|
||||||
|
// See .env.testing which sets USERPREF_LOCKED___FORMAT_PASSWORD=false
|
||||||
|
$lockedPreferences = Helpers::lockedPreferences(config('2fauth.preferences'));
|
||||||
|
|
||||||
|
$this->assertNotContains('formatPassword', $lockedPreferences);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
2
vite.version.js
vendored
2
vite.version.js
vendored
@ -12,6 +12,6 @@ const parser = new Engine({
|
|||||||
|
|
||||||
const phpFile = fs.readFileSync("./config/2fauth.php")
|
const phpFile = fs.readFileSync("./config/2fauth.php")
|
||||||
const phpContent = parser.parseCode(phpFile)
|
const phpContent = parser.parseCode(phpFile)
|
||||||
const version = phpContent.children[0].expr.items[0].value.value
|
const version = phpContent.children[4].expr.items[0].value.value
|
||||||
|
|
||||||
export default version
|
export default version
|
Loading…
x
Reference in New Issue
Block a user