mirror of
https://github.com/Bubka/2FAuth.git
synced 2025-06-20 11:47:53 +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_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_HOST=localhost
|
||||
# DB_PORT=3306
|
||||
|
@ -35,6 +35,7 @@ class UserController extends Controller
|
||||
$jsonPrefs->push([
|
||||
'key' => $key,
|
||||
'value' => $item,
|
||||
'locked' => in_array($key, config('2fauth.lockedPreferences')),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -55,6 +56,7 @@ class UserController extends Controller
|
||||
return response()->json([
|
||||
'key' => $preferenceName,
|
||||
'value' => $request->user()->preferences[$preferenceName],
|
||||
'locked' => in_array($preferenceName, config('2fauth.lockedPreferences')),
|
||||
], 200);
|
||||
}
|
||||
|
||||
@ -69,6 +71,10 @@ class UserController extends Controller
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if (in_array($preferenceName, config('2fauth.lockedPreferences'))) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$validated = $request->validated();
|
||||
|
||||
$request->user()['preferences->' . $preferenceName] = $validated['value'];
|
||||
|
@ -49,4 +49,22 @@ class Helpers
|
||||
{
|
||||
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;
|
||||
$subdir = config('2fauth.config.appSubdirectory') ? '/' . config('2fauth.config.appSubdirectory') : '';
|
||||
$defaultPreferences = collect(config('2fauth.preferences')); /** @phpstan-ignore-line */
|
||||
$lockedPreferences = collect(config('2fauth.lockedPreferences')); /** @phpstan-ignore-line */
|
||||
$isDemoApp = config('2fauth.config.isDemoApp') ? 'true' : 'false';
|
||||
$isTestingApp = config('2fauth.config.isTestingApp') ? 'true' : 'false';
|
||||
$lang = App::getLocale();
|
||||
@ -69,6 +70,7 @@ class SinglePageController extends Controller
|
||||
'exportSchemaUrl' => $exportSchemaUrl,
|
||||
]),
|
||||
'defaultPreferences' => $defaultPreferences,
|
||||
'lockedPreferences' => $lockedPreferences,
|
||||
'subdirectory' => $subdir,
|
||||
'isDemoApp' => $isDemoApp,
|
||||
'isTestingApp' => $isTestingApp,
|
||||
|
@ -13,15 +13,12 @@
|
||||
|
||||
if (! function_exists('envUnlessEmpty')) {
|
||||
/**
|
||||
* @param string $key
|
||||
* @param null $default
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
function envUnlessEmpty(string $key, $default = null)
|
||||
function envUnlessEmpty(string $key, string|int|bool|float|null $default = null)
|
||||
{
|
||||
$result = env($key, $default);
|
||||
if (is_string($result) && '' === $result) {
|
||||
if ('' === $result) {
|
||||
$result = $default;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,47 @@
|
||||
<?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 [
|
||||
|
||||
/*
|
||||
@ -107,37 +149,17 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'preferences' => [
|
||||
'showOtpAsDot' => false,
|
||||
'showNextOtp' => false,
|
||||
'revealDottedOTP' => false,
|
||||
'closeOtpOnCopy' => false,
|
||||
'copyOtpOnDisplay' => false,
|
||||
'clearSearchOnCopy' => false,
|
||||
'useBasicQrcodeReader' => false,
|
||||
'displayMode' => 'list',
|
||||
'showAccountsIcons' => true,
|
||||
'kickUserAfter' => 15,
|
||||
'activeGroup' => 0,
|
||||
'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,
|
||||
],
|
||||
'preferences' => $preferences,
|
||||
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| List of user preferences locked against user customization
|
||||
| These settings cannot be overloaded and persisted by each user
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
*/
|
||||
|
||||
'lockedPreferences' => Helpers::lockedPreferences(Arr::except($preferences, $nonLockablePreferences)),
|
||||
|
||||
];
|
||||
|
@ -26,6 +26,7 @@
|
||||
},
|
||||
isIndented: Boolean,
|
||||
isDisabled: Boolean,
|
||||
isLocked: Boolean,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
@ -49,12 +50,14 @@
|
||||
|
||||
<template>
|
||||
<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"/>
|
||||
</div>
|
||||
<div>
|
||||
<input :id="fieldName" type="checkbox" :name="fieldName" class="is-checkradio is-info" v-model="model" :disabled="isDisabled" :aria-describedby="help ? legendId : undefined" />
|
||||
<label tabindex="0" :for="fieldName" class="label" :class="labelClass" v-html="$t(label)" v-on:keypress.space.prevent="toggleModel" />
|
||||
<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-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" />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -42,6 +42,7 @@
|
||||
default: null
|
||||
},
|
||||
isIndented: Boolean,
|
||||
isLocked: Boolean,
|
||||
leftIcon: '',
|
||||
rightIcon: '',
|
||||
idSuffix: {
|
||||
@ -57,14 +58,16 @@
|
||||
|
||||
<template>
|
||||
<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"/>
|
||||
</div>
|
||||
<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 }">
|
||||
<input
|
||||
:disabled="isDisabled"
|
||||
:disabled="isDisabled || isLocked"
|
||||
:id="inputId"
|
||||
:type="inputType"
|
||||
class="input"
|
||||
|
@ -54,14 +54,15 @@
|
||||
idSuffix: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
isLocked: Boolean,
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
|
||||
const { valErrorId } = useValidationErrorIdGenerator(props.fieldName)
|
||||
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 componentKey = ref(0);
|
||||
|
||||
@ -96,7 +97,9 @@
|
||||
</script>
|
||||
|
||||
<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="control" :class="{ 'is-expanded': isExpanded }">
|
||||
<input
|
||||
|
@ -45,6 +45,7 @@
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
isLocked: Boolean,
|
||||
})
|
||||
|
||||
const { inputId } = useIdGenerator(props.inputType, props.fieldName + props.idSuffix)
|
||||
@ -84,10 +85,12 @@
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<input
|
||||
:disabled="isDisabled"
|
||||
:disabled="isDisabled || isLocked"
|
||||
:id="inputId"
|
||||
:type="currentType"
|
||||
class="input"
|
||||
|
@ -23,6 +23,7 @@
|
||||
},
|
||||
isIndented: Boolean,
|
||||
isDisabled: Boolean,
|
||||
isLocked: Boolean,
|
||||
idSuffix: {
|
||||
type: String,
|
||||
default: ''
|
||||
@ -37,18 +38,20 @@
|
||||
|
||||
<template>
|
||||
<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"/>
|
||||
</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="select">
|
||||
<select
|
||||
:id="inputId"
|
||||
v-model="selected"
|
||||
v-on:change="$emit('update:modelValue', $event.target.value)"
|
||||
:disabled="isDisabled"
|
||||
:disabled="isDisabled || isLocked"
|
||||
:aria-describedby="help ? legendId : undefined"
|
||||
:aria-invalid="fieldError != undefined"
|
||||
:aria-errormessage="fieldError != undefined ? valErrorId : undefined"
|
||||
|
@ -42,6 +42,7 @@
|
||||
default: null
|
||||
},
|
||||
isIndented: Boolean,
|
||||
isLocked: Boolean,
|
||||
leftIcon: '',
|
||||
rightIcon: '',
|
||||
idSuffix: {
|
||||
@ -57,14 +58,16 @@
|
||||
|
||||
<template>
|
||||
<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"/>
|
||||
</div>
|
||||
<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 }">
|
||||
<textarea
|
||||
:disabled="isDisabled"
|
||||
:disabled="isDisabled || isLocked"
|
||||
:id="inputId"
|
||||
class="textarea"
|
||||
:class="size"
|
||||
|
@ -15,6 +15,7 @@
|
||||
fieldError: [String],
|
||||
hasOffset: Boolean,
|
||||
isDisabled: Boolean,
|
||||
isLocked: Boolean,
|
||||
label: {
|
||||
type: String,
|
||||
default: ''
|
||||
@ -38,7 +39,9 @@
|
||||
|
||||
<template>
|
||||
<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
|
||||
id="rdoGroup"
|
||||
role="radiogroup"
|
||||
@ -56,7 +59,7 @@
|
||||
type="button"
|
||||
class="button"
|
||||
:aria-checked="modelValue===choice.value"
|
||||
:disabled="isDisabled"
|
||||
:disabled="isDisabled || isLocked"
|
||||
:class="{
|
||||
'is-link': modelValue===choice.value,
|
||||
'is-dark': mode==='dark',
|
||||
@ -69,7 +72,7 @@
|
||||
class="is-hidden"
|
||||
:checked="modelValue===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" />
|
||||
<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',
|
||||
|
||||
state: () => {
|
||||
return { ...window.appSettings }
|
||||
return {
|
||||
lockedPreferences: window.lockedPreferences,
|
||||
...window.appSettings
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
*/
|
||||
refreshPreferences() {
|
||||
const appSettings = useAppSettingsStore()
|
||||
|
||||
userService.getPreferences({returnError: true})
|
||||
.then(response => {
|
||||
response.data.forEach(preference => {
|
||||
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 => {
|
||||
|
@ -4,12 +4,14 @@
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { useGroups } from '@/stores/groups'
|
||||
import { useNotifyStore } from '@/stores/notify'
|
||||
import { useAppSettingsStore } from '@/stores/appSettings'
|
||||
import { timezones } from './timezones'
|
||||
|
||||
const $2fauth = inject('2fauth')
|
||||
const user = useUserStore()
|
||||
const groups = useGroups()
|
||||
const notify = useNotifyStore()
|
||||
const appSettings = useAppSettingsStore()
|
||||
const returnTo = useStorage($2fauth.prefix + 'returnTo', 'accounts')
|
||||
|
||||
const layouts = [
|
||||
@ -121,8 +123,11 @@
|
||||
<!-- user preferences -->
|
||||
<div class="block">
|
||||
<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 -->
|
||||
<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">
|
||||
{{ $t('settings.forms.some_translation_are_missing') }}
|
||||
<a class="ml-2" href="https://crowdin.com/project/2fauth">
|
||||
@ -131,66 +136,66 @@
|
||||
</a>
|
||||
</div>
|
||||
<!-- 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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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" />
|
||||
<FormToggle v-model="user.preferences.formatPasswordBy" @update:model-value="val => savePreference('formatPasswordBy', val)" :choices="passwordFormats" fieldName="formatPasswordBy" :isDisabled="!user.preferences.formatPassword" />
|
||||
<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" :isLocked="appSettings.lockedPreferences.includes('formatPasswordBy')" :isDisabled="!user.preferences.formatPassword" />
|
||||
<!-- 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 -->
|
||||
<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 -->
|
||||
<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>
|
||||
<!-- 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" />
|
||||
<!-- 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 -->
|
||||
<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>
|
||||
<!-- 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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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>
|
||||
<!-- 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 -->
|
||||
<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>
|
||||
<!-- 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 -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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>
|
||||
</form>
|
||||
</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!',
|
||||
'data_input' => 'Data input',
|
||||
'settings_managed_by_administrator' => 'Some settings are being managed by your administrator',
|
||||
'forms' => [
|
||||
'edit_settings' => 'Edit settings',
|
||||
'setting_saved' => 'Settings saved',
|
||||
@ -58,23 +59,23 @@ return [
|
||||
'help' => 'The time zone applied to all dates and times displayed in the application'
|
||||
],
|
||||
'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'
|
||||
],
|
||||
'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'
|
||||
],
|
||||
'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'
|
||||
],
|
||||
'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)'
|
||||
],
|
||||
'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.'
|
||||
],
|
||||
'clear_search_on_copy' => [
|
||||
@ -86,7 +87,7 @@ return [
|
||||
'help' => 'When invoked, force the Sort function to sort accounts on a case-sensitive basis'
|
||||
],
|
||||
'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'
|
||||
],
|
||||
'use_basic_qrcode_reader' => [
|
||||
|
@ -78,7 +78,7 @@ return [
|
||||
],
|
||||
'plain_text' => 'Plain text',
|
||||
'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'
|
||||
],
|
||||
'digits' => [
|
||||
|
1
resources/views/landing.blade.php
vendored
1
resources/views/landing.blade.php
vendored
@ -27,6 +27,7 @@
|
||||
var appConfig = {!! $appConfig !!};
|
||||
var urls = {!! $urls !!};
|
||||
var defaultPreferences = {!! $defaultPreferences->toJson() !!};
|
||||
var lockedPreferences = {!! $lockedPreferences->toJson() !!};
|
||||
var appVersion = '{{ config("2fauth.version") }}';
|
||||
var isDemoApp = {!! $isDemoApp !!};
|
||||
var isTestingApp = {!! $isTestingApp !!};
|
||||
|
@ -24,6 +24,7 @@ class UserControllerTest extends FeatureTestCase
|
||||
private const PREFERENCE_JSON_STRUCTURE = [
|
||||
'key',
|
||||
'value',
|
||||
'locked',
|
||||
];
|
||||
|
||||
public function setUp() : void
|
||||
@ -62,7 +63,7 @@ class UserControllerTest extends FeatureTestCase
|
||||
}
|
||||
|
||||
#[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')
|
||||
->json('GET', '/api/v1/user/preferences')
|
||||
@ -108,7 +109,7 @@ class UserControllerTest extends FeatureTestCase
|
||||
}
|
||||
|
||||
#[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
|
||||
@ -121,6 +122,28 @@ class UserControllerTest extends FeatureTestCase
|
||||
->assertExactJson([
|
||||
'key' => '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);
|
||||
}
|
||||
|
||||
#[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 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
|
Loading…
x
Reference in New Issue
Block a user