Sync app settings at login/registration instead of page load

This commit is contained in:
Bubka 2025-02-26 18:32:20 +01:00
parent 4ef76e0ae9
commit f3945463b7
11 changed files with 71 additions and 40 deletions

View File

@ -19,7 +19,19 @@ public function index()
{
event(new ScanForNewReleaseCalled);
$settings = Settings::all()->toJson();
// We only share necessary and acceptable values with the HTML front-end.
// But all the properties have to be pushed to init the appSetting store state correctly,
// so we set them to null, they will be fed later by the front-end
$appSettings = Settings::all();
$publicSettings = $appSettings->only([
'disableRegistration',
'enableSso',
'useSsoOnly'
]);
$settings = $appSettings->map(function (mixed $item, string $key) {
return null;
})->merge($publicSettings)->toJson();
$proxyAuth = config('auth.defaults.guard') === 'reverse-proxy-guard' ? true : false;
$proxyLogoutUrl = config('2fauth.config.proxyLogoutUrl') ? config('2fauth.config.proxyLogoutUrl') : false;
$subdir = config('2fauth.config.appSubdirectory') ? '/' . config('2fauth.config.appSubdirectory') : '';

View File

@ -1,5 +1,4 @@
import appSettingService from '@/services/appSettingService'
import { useAppSettingsStore } from '@/stores/appSettings'
import { useNotifyStore } from '@/stores/notify'
/**
@ -9,13 +8,11 @@ import { useNotifyStore } from '@/stores/notify'
*/
export async function useAppSettingsUpdater(setting, value, returnValidationError = false) {
// const appSettings = useAppSettingsStore()
let data = null
let error = null
await appSettingService.update(setting, value, { returnError: true })
.then(response => {
// appSettings[setting] = value
data = value
useNotifyStore().success({ type: 'is-success', text: trans('settings.forms.setting_saved') })
})

View File

@ -79,7 +79,7 @@
</ul>
<!-- email link -->
<button type="button" id="btnEmailMenu" @click="showMenu = !showMenu" class="button is-text is-like-text has-text-grey" style="width: 100%;">
<span v-if="appSettings.latestRelease && appSettings.checkForUpdate" class="release-flag"></span>
<span v-if="user.isAdmin && appSettings.latestRelease && appSettings.checkForUpdate" class="release-flag"></span>
<span class="mx-2 has-ellipsis">{{ user.email }}</span>
<FontAwesomeIcon v-if="!showMenu" :icon="['fas', 'bars']" class="mr-2" />
<!-- <button v-else class="delete ml-3"></button> -->

View File

@ -5,42 +5,43 @@ import { useTwofaccounts } from '@/stores/twofaccounts'
import { useAppSettingsStore } from '@/stores/appSettings'
import { useNotifyStore } from '@/stores/notify'
import authGuard from './middlewares/authGuard'
import adminOnly from './middlewares/adminOnly'
import starter from './middlewares/starter'
import noEmptyError from './middlewares/noEmptyError'
import noRegistration from './middlewares/noRegistration'
import setReturnTo from './middlewares/setReturnTo'
import authGuard from './middlewares/authGuard'
import adminOnly from './middlewares/adminOnly'
import starter from './middlewares/starter'
import noEmptyError from './middlewares/noEmptyError'
import noRegistration from './middlewares/noRegistration'
import setReturnTo from './middlewares/setReturnTo'
import skipIfAuthProxy from './middlewares/skipIfAuthProxy'
import syncAppSettings from './middlewares/syncAppSettings'
const router = createRouter({
history: createWebHistory(window.appConfig.subdirectory ? window.appConfig.subdirectory : '/'),
routes: [
{ path: '/start', name: 'start', component: () => import('../views/Start.vue'), meta: { middlewares: [authGuard, setReturnTo], watchedByKicker: true } },
{ path: '/capture', name: 'capture', component: () => import('../views/twofaccounts/Capture.vue'), meta: { middlewares: [authGuard, setReturnTo], watchedByKicker: true } },
{ path: '/start', name: 'start', component: () => import('../views/Start.vue'), meta: { middlewares: [authGuard, syncAppSettings, setReturnTo], watchedByKicker: true } },
{ path: '/capture', name: 'capture', component: () => import('../views/twofaccounts/Capture.vue'), meta: { middlewares: [authGuard, syncAppSettings, setReturnTo], watchedByKicker: true } },
{ path: '/accounts', name: 'accounts', component: () => import('../views/twofaccounts/Accounts.vue'), meta: { middlewares: [authGuard, starter, setReturnTo], watchedByKicker: true }, alias: '/' },
{ path: '/account/create', name: 'createAccount', component: () => import('../views/twofaccounts/CreateUpdate.vue'), meta: { middlewares: [authGuard, setReturnTo], watchedByKicker: true } },
{ path: '/account/import', name: 'importAccounts', component: () => import('../views/twofaccounts/Import.vue'), meta: { middlewares: [authGuard, setReturnTo], watchedByKicker: true } },
{ path: '/account/:twofaccountId/edit', name: 'editAccount', component: () => import('../views/twofaccounts/CreateUpdate.vue'), meta: { middlewares: [authGuard, setReturnTo], watchedByKicker: true }, props: true },
{ path: '/account/:twofaccountId/qrcode', name: 'showQRcode', component: () => import('../views/twofaccounts/QRcode.vue'), meta: { middlewares: [authGuard, setReturnTo], watchedByKicker: true } },
{ path: '/accounts', name: 'accounts', component: () => import('../views/twofaccounts/Accounts.vue'), meta: { middlewares: [authGuard, syncAppSettings, starter, setReturnTo], watchedByKicker: true }, alias: '/' },
{ path: '/account/create', name: 'createAccount', component: () => import('../views/twofaccounts/CreateUpdate.vue'), meta: { middlewares: [authGuard, syncAppSettings, setReturnTo], watchedByKicker: true } },
{ path: '/account/import', name: 'importAccounts', component: () => import('../views/twofaccounts/Import.vue'), meta: { middlewares: [authGuard, syncAppSettings, setReturnTo], watchedByKicker: true } },
{ path: '/account/:twofaccountId/edit', name: 'editAccount', component: () => import('../views/twofaccounts/CreateUpdate.vue'), meta: { middlewares: [authGuard, syncAppSettings, setReturnTo], watchedByKicker: true }, props: true },
{ path: '/account/:twofaccountId/qrcode', name: 'showQRcode', component: () => import('../views/twofaccounts/QRcode.vue'), meta: { middlewares: [authGuard, syncAppSettings, setReturnTo], watchedByKicker: true } },
{ path: '/groups', name: 'groups', component: () => import('../views/groups/Groups.vue'), meta: { middlewares: [authGuard, setReturnTo], watchedByKicker: true }, props: true },
{ path: '/group/create', name: 'createGroup', component: () => import('../views/groups/CreateUpdate.vue'), meta: { middlewares: [authGuard, setReturnTo], watchedByKicker: true } },
{ path: '/group/:groupId/edit', name: 'editGroup', component: () => import('../views/groups/CreateUpdate.vue'), meta: { middlewares: [authGuard, setReturnTo], watchedByKicker: true }, props: true },
{ path: '/groups', name: 'groups', component: () => import('../views/groups/Groups.vue'), meta: { middlewares: [authGuard, syncAppSettings, setReturnTo], watchedByKicker: true }, props: true },
{ path: '/group/create', name: 'createGroup', component: () => import('../views/groups/CreateUpdate.vue'), meta: { middlewares: [authGuard, syncAppSettings, setReturnTo], watchedByKicker: true } },
{ path: '/group/:groupId/edit', name: 'editGroup', component: () => import('../views/groups/CreateUpdate.vue'), meta: { middlewares: [authGuard, syncAppSettings, setReturnTo], watchedByKicker: true }, props: true },
{ path: '/settings/options', name: 'settings.options', component: () => import('../views/settings/Options.vue'), meta: { middlewares: [authGuard], watchedByKicker: true, showAbout: true } },
{ path: '/settings/account', name: 'settings.account', component: () => import('../views/settings/Account.vue'), meta: { middlewares: [authGuard], watchedByKicker: true, showAbout: true } },
{ path: '/settings/oauth', name: 'settings.oauth.tokens', component: () => import('../views/settings/OAuth.vue'), meta: { middlewares: [authGuard], watchedByKicker: true, showAbout: true, props: true } },
{ path: '/settings/webauthn/:credentialId/edit', name: 'settings.webauthn.editCredential', component: () => import('../views/settings/Credentials/Edit.vue'), meta: { middlewares: [authGuard], watchedByKicker: true, showAbout: true }, props: true },
{ path: '/settings/webauthn', name: 'settings.webauthn.devices', component: () => import('../views/settings/WebAuthn.vue'), meta: { middlewares: [authGuard], watchedByKicker: true, showAbout: true } },
{ path: '/settings/options', name: 'settings.options', component: () => import('../views/settings/Options.vue'), meta: { middlewares: [authGuard, syncAppSettings], watchedByKicker: true, showAbout: true } },
{ path: '/settings/account', name: 'settings.account', component: () => import('../views/settings/Account.vue'), meta: { middlewares: [authGuard, syncAppSettings], watchedByKicker: true, showAbout: true } },
{ path: '/settings/oauth', name: 'settings.oauth.tokens', component: () => import('../views/settings/OAuth.vue'), meta: { middlewares: [authGuard, syncAppSettings], watchedByKicker: true, showAbout: true, props: true } },
{ path: '/settings/webauthn/:credentialId/edit', name: 'settings.webauthn.editCredential', component: () => import('../views/settings/Credentials/Edit.vue'), meta: { middlewares: [authGuard, syncAppSettings], watchedByKicker: true, showAbout: true }, props: true },
{ path: '/settings/webauthn', name: 'settings.webauthn.devices', component: () => import('../views/settings/WebAuthn.vue'), meta: { middlewares: [authGuard, syncAppSettings], watchedByKicker: true, showAbout: true } },
{ path: '/admin/app', name: 'admin.appSetup', component: () => import('../views/admin/AppSetup.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } },
{ path: '/admin/auth', name: 'admin.auth', component: () => import('../views/admin/Auth.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } },
{ path: '/admin/users', name: 'admin.users', component: () => import('../views/admin/Users.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } },
{ path: '/admin/users/create', name: 'admin.createUser', component: () => import('../views/admin/users/Create.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } },
{ path: '/admin/users/:userId/manage', name: 'admin.manageUser', component: () => import('../views/admin/users/Manage.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true }, props: true },
{ path: '/admin/logs/:userId/access', name: 'admin.logs.access', component: () => import('../views/admin/logs/Access.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true }, props: true },
{ path: '/admin/users', name: 'admin.users', component: () => import('../views/admin/Users.vue'), meta: { middlewares: [authGuard, syncAppSettings, adminOnly], watchedByKicker: true, showAbout: true } },
{ path: '/admin/users/create', name: 'admin.createUser', component: () => import('../views/admin/users/Create.vue'), meta: { middlewares: [authGuard, syncAppSettings, adminOnly], watchedByKicker: true, showAbout: true } },
{ path: '/admin/users/:userId/manage', name: 'admin.manageUser', component: () => import('../views/admin/users/Manage.vue'), meta: { middlewares: [authGuard, syncAppSettings, adminOnly], watchedByKicker: true, showAbout: true }, props: true },
{ path: '/admin/logs/:userId/access', name: 'admin.logs.access', component: () => import('../views/admin/logs/Access.vue'), meta: { middlewares: [authGuard, syncAppSettings, adminOnly], watchedByKicker: true, showAbout: true }, props: true },
{ path: '/login', name: 'login', component: () => import('../views/auth/Login.vue'), meta: { middlewares: [skipIfAuthProxy, setReturnTo], showAbout: true } },
{ path: '/register', name: 'register', component: () => import('../views/auth/Register.vue'), meta: { middlewares: [skipIfAuthProxy, noRegistration, setReturnTo], showAbout: true } },

View File

@ -0,0 +1,12 @@
/**
* Retrieve app settings from the backend, only if the store is not synced yet
*/
export default function syncAppSettings({ to, next, nextMiddleware, stores }) {
const { appSettings, user } = stores
if (user.isAdmin && ! appSettings.isSynced ) {
appSettings.fetch()
}
nextMiddleware()
}

View File

@ -9,20 +9,25 @@ export const useAppSettingsStore = defineStore({
return { ...window.appSettings }
},
actions: {
getters: {
// Tells if all properties have been fetched from the backend.
// Here we test useEncryption but we could have test any other property
// appart from the ones pushed by Laravel in the html template.
isSynced: (state) => state.useEncryption != null,
},
actions: {
/**
* Fetches the appSetting collection from the backend
*/
async fetch() {
appSettingService.getAll({ returnError: true })
.then(response => {
appSettingService.getAll({ returnError: true }).then(response => {
response.data.forEach(setting => {
this[setting.key] = setting.value
})
})
.catch(error => {
useNotifyStore().alert({ text: trans('errors.data_cannot_be_refreshed_from_server') })
useNotifyStore().alert({ text: trans('errors.failed_to_retrieve_app_settings') })
})
},
},

View File

@ -6,6 +6,7 @@ import { useColorMode } from '@vueuse/core'
import { useTwofaccounts } from '@/stores/twofaccounts'
import { useGroups } from '@/stores/groups'
import { useNotifyStore } from '@/stores/notify'
import { useAppSettingsStore } from '@/stores/appSettings'
export const useUserStore = defineStore({
id: 'user',
@ -98,6 +99,7 @@ export const useUserStore = defineStore({
this.$reset()
this.initDataStores()
this.applyUserPrefs()
useAppSettingsStore().$reset()
router.push({ name: 'login' })
},

View File

@ -27,8 +27,6 @@
activeForm.value = 'webauthn'
}
else activeForm.value = 'legacy'
// showWebauthnForm && appSettings.useSsoOnly != true
})
@ -47,6 +45,7 @@
*/
function LegacysignIn(e) {
notify.clear()
isBusy.value = true
form.post('/user/login', {returnError: true}).then(async (response) => {
await user.loginAs({
@ -69,6 +68,9 @@
notify.error(error)
}
})
.finally(() => {
isBusy.value = false
})
}
/**
@ -195,7 +197,7 @@
<form id="frmLegacyLogin" @submit.prevent="LegacysignIn" @keydown="form.onKeydown($event)">
<FormField v-model="form.email" fieldName="email" :fieldError="form.errors.get('email')" inputType="email" label="auth.forms.email" autocomplete="username" autofocus />
<FormPasswordField v-model="form.password" fieldName="password" :fieldError="form.errors.get('password')" label="auth.forms.password" autocomplete="current-password" />
<FormButtons :isBusy="form.isBusy" caption="auth.sign_in" submitId="btnSignIn"/>
<FormButtons :isBusy="isBusy" caption="auth.sign_in" submitId="btnSignIn"/>
</form>
<div class="nav-links">
<p>{{ $t('auth.forms.forgot_your_password') }}&nbsp;

View File

@ -33,6 +33,7 @@
email: response.data.email,
preferences: response.data.preferences,
isAdmin: response.data.is_admin ?? false,
// TODO : add 'id' to the response
})
user.applyTheme()

View File

@ -15,7 +15,6 @@
import { useBusStore } from '@/stores/bus'
import { useTwofaccounts } from '@/stores/twofaccounts'
import { useGroups } from '@/stores/groups'
import { useAppSettingsStore } from '@/stores/appSettings'
import { useDisplayablePassword } from '@/composables/helpers'
import { useSortable, moveArrayElement } from '@vueuse/integrations/useSortable'
@ -24,7 +23,6 @@
const notify = useNotifyStore()
const user = useUserStore()
const bus = useBusStore()
const appSettings = useAppSettingsStore()
const { copy, copied } = useClipboard({ legacy: true })
const twofaccounts = useTwofaccounts()
const groups = useGroups()
@ -407,7 +405,7 @@
<div class="tfa-text has-ellipsis">
<img v-if="account.icon && user.preferences.showAccountsIcons" role="presentation" class="tfa-icon" :src="$2fauth.config.subdirectory + '/storage/icons/' + account.icon" alt="">
<img v-else-if="account.icon == null && user.preferences.showAccountsIcons" role="presentation" class="tfa-icon" :src="$2fauth.config.subdirectory + '/storage/noicon.svg'" alt="">
{{ account.service ? account.service : $t('twofaccounts.no_service') }}<FontAwesomeIcon class="has-text-danger is-size-5 ml-2" v-if="appSettings.useEncryption && account.account === $t('errors.indecipherable')" :icon="['fas', 'exclamation-circle']" />
{{ account.service ? account.service : $t('twofaccounts.no_service') }}<FontAwesomeIcon class="has-text-danger is-size-5 ml-2" v-if="account.account === $t('errors.indecipherable')" :icon="['fas', 'exclamation-circle']" />
<span class="has-ellipsis is-family-primary is-size-6 is-size-7-mobile has-text-grey ">{{ account.account }}</span>
</div>
</div>

View File

@ -74,4 +74,5 @@
'qrcode_has_invalid_checksum' => 'QR code has invalid checksum',
'no_readable_qrcode' => 'No readable QR code',
'failed_icon_store_database_toggling' => 'Migration of icons failed. The setting has been restored to its previous value.',
'failed_to_retrieve_app_settings' => 'Failed to retrieve application settings'
];