Jelajahi Sumber

Sync app settings at login/registration instead of page load

Bubka 5 bulan lalu
induk
melakukan
f3945463b7

+ 13 - 1
app/Http/Controllers/SinglePageController.php

@@ -19,7 +19,19 @@ class SinglePageController extends Controller
     {
         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') : '';

+ 0 - 3
resources/js/composables/appSettingsUpdater.js

@@ -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') })
     })

+ 1 - 1
resources/js/layouts/Footer.vue

@@ -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> -->

+ 26 - 25
resources/js/router/index.js

@@ -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 } },

+ 12 - 0
resources/js/router/middlewares/syncAppSettings.js

@@ -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()
+}

+ 9 - 4
resources/js/stores/appSettings.js

@@ -9,20 +9,25 @@ export const useAppSettingsStore = defineStore({
         return { ...window.appSettings }
     },
 
+    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') })
             })
         },
     },

+ 2 - 0
resources/js/stores/user.js

@@ -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' })
         },
 

+ 5 - 3
resources/js/views/auth/Login.vue

@@ -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;

+ 1 - 0
resources/js/views/auth/Register.vue

@@ -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()
 

+ 1 - 3
resources/js/views/twofaccounts/Accounts.vue

@@ -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>

+ 1 - 0
resources/lang/en/errors.php

@@ -74,4 +74,5 @@ return [
     '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'
 ];