Set up a global notification handler & Error view & Modal component

This commit is contained in:
Bubka 2023-09-28 11:27:45 +02:00
parent e37c0c9ea5
commit 73e36edd9c
7 changed files with 198 additions and 12 deletions

View File

@ -1,8 +1,5 @@
import '/resources/js_vue3/assets/app.scss'; import '/resources/js_vue3/assets/app.scss';
// import { createApp } from 'vue'
// import { i18nVue } from 'laravel-vue-i18n'
// import { createPinia } from 'pinia'
import Notifications from '@kyvg/vue3-notification' import Notifications from '@kyvg/vue3-notification'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
@ -13,7 +10,7 @@ const app = createApp(App)
// Immutable app properties provided by the laravel blade view // Immutable app properties provided by the laravel blade view
const $2fauth = { const $2fauth = {
prefix: '2fauth_', prefix: '2fauth_',
config: window.appConfig, //{"proxyAuth":false,"proxyLogoutUrl":false,"subdirectory":""} config: window.appConfig,
version: window.appVersion, version: window.appVersion,
isDemoApp: window.isDemoApp, isDemoApp: window.isDemoApp,
isTestingApp: window.isTestingApp, isTestingApp: window.isTestingApp,
@ -21,13 +18,17 @@ const $2fauth = {
} }
app.provide('2fauth', readonly($2fauth)) app.provide('2fauth', readonly($2fauth))
// Stores
const pinia = createPinia() const pinia = createPinia()
pinia.use(({ store }) => { pinia.use(({ store }) => {
store.$2fauth = $2fauth; store.$2fauth = $2fauth;
}); });
app.use(pinia) app.use(pinia)
// Router
app.use(router) app.use(router)
// Localization
app.use(i18nVue, { app.use(i18nVue, {
lang: document.documentElement.lang.substring(0, 2), lang: document.documentElement.lang.substring(0, 2),
resolve: async lang => { resolve: async lang => {
@ -39,26 +40,41 @@ app.use(i18nVue, {
}) })
app.use(Notifications) app.use(Notifications)
// Components registration
import ResponsiveWidthWrapper from '@/layouts/ResponsiveWidthWrapper.vue' import ResponsiveWidthWrapper from '@/layouts/ResponsiveWidthWrapper.vue'
import FormWrapper from '@/layouts/FormWrapper.vue' import FormWrapper from '@/layouts/FormWrapper.vue'
import Footer from '@/layouts/Footer.vue' import Footer from '@/layouts/Footer.vue'
import Modal from '@/layouts/Modal.vue'
import VueButton from '@/components/formElements/Button.vue' import VueButton from '@/components/formElements/Button.vue'
import FieldError from '@/components/formElements/FieldError.vue' import FieldError from '@/components/formElements/FieldError.vue'
import FormField from '@/components/formElements/FormField.vue' import FormField from '@/components/formElements/FormField.vue'
import FormPasswordField from '@/components/formElements/FormPasswordField.vue' import FormPasswordField from '@/components/formElements/FormPasswordField.vue'
// import FormSelect from './FormSelect'
// import FormSwitch from './FormSwitch'
// import FormToggle from './FormToggle'
// import FormCheckbox from './FormCheckbox'
import FormButtons from '@/components/formElements/FormButtons.vue' import FormButtons from '@/components/formElements/FormButtons.vue'
// import Kicker from './Kicker'
// import SettingTabs from './SettingTabs'
// Components registration
app app
.component('FontAwesomeIcon', FontAwesomeIcon) .component('FontAwesomeIcon', FontAwesomeIcon)
.component('ResponsiveWidthWrapper', ResponsiveWidthWrapper) .component('ResponsiveWidthWrapper', ResponsiveWidthWrapper)
.component('FormWrapper', FormWrapper) .component('FormWrapper', FormWrapper)
.component('VueFooter', Footer) .component('VueFooter', Footer)
.component('Modal', Modal)
.component('VueButton', VueButton) .component('VueButton', VueButton)
.component('FieldError', FieldError) .component('FieldError', FieldError)
.component('FormField', FormField) .component('FormField', FormField)
.component('FormPasswordField', FormPasswordField) .component('FormPasswordField', FormPasswordField)
.component('FormButtons', FormButtons) .component('FormButtons', FormButtons)
// Global error handling
import { useNotifyStore } from '@/stores/notify'
app.config.errorHandler = (err, instance, info) => {
useNotifyStore().error(err)
}
// App mounting // App mounting
app.mount('#app') app.mount('#app')

View File

@ -0,0 +1,52 @@
<script setup>
const { notify } = useNotification()
const props = defineProps({
modelValue: Boolean,
closable: {
type: Boolean,
default: true
},
})
const emit = defineEmits(['modalClosed'])
const isActive = computed({
get() {
return props.modelValue
},
set(value) {
emit('modalClosed')
}
})
function closeModal(event) {
if (event) {
notify({ clean: true })
isActive.value = false
}
}
</script>
<template>
<div class="modal modal-otp" v-bind:class="{ 'is-active': isActive }">
<div class="modal-background" @click.stop="closeModal"></div>
<div class="modal-content">
<section class="section">
<div class="columns is-centered">
<div class="column is-three-quarters">
<div class="modal-slot box has-text-centered is-shadowless">
<slot></slot>
</div>
</div>
</div>
</section>
</div>
<div v-if="props.closable" class="fullscreen-footer">
<!-- Close button -->
<button id="btnClose" class="button is-rounded" :class="{'is-dark' : false}" @click.stop="closeModal">
{{ $t('commons.close') }}
</button>
</div>
</div>
</template>

View File

@ -25,10 +25,11 @@ import SettingsOptions from '../views/settings/Options.vue'
// import SettingsWebAuthn from './views/settings/WebAuthn.vue' // import SettingsWebAuthn from './views/settings/WebAuthn.vue'
// import EditCredential from './views/settings/Credentials/Edit.vue' // import EditCredential from './views/settings/Credentials/Edit.vue'
// import GeneratePAT from './views/settings/PATokens/Create.vue' // import GeneratePAT from './views/settings/PATokens/Create.vue'
// import Errors from './views/Error.vue' import Errors from '../views/Error.vue'
import About from '../views/About.vue' import About from '../views/About.vue'
import authGuard from './middlewares/authGuard' import authGuard from './middlewares/authGuard'
import noEmptyError from './middlewares/noEmptyError'
const router = createRouter({ const router = createRouter({
history: createWebHistory('/'), history: createWebHistory('/'),
@ -61,8 +62,8 @@ const router = createRouter({
{ path: '/webauthn/lost', name: 'webauthn.lost', component: WebauthnLost, meta: { disabledWithAuthProxy: true, showAbout: true } }, { path: '/webauthn/lost', name: 'webauthn.lost', component: WebauthnLost, meta: { disabledWithAuthProxy: true, showAbout: true } },
// { path: '/webauthn/recover', name: 'webauthn.recover', component: WebauthnRecover, meta: { disabledWithAuthProxy: true, showAbout: true } }, // { path: '/webauthn/recover', name: 'webauthn.recover', component: WebauthnRecover, meta: { disabledWithAuthProxy: true, showAbout: true } },
{ path: '/about', name: 'about',component: About, meta: { showAbout: true } }, { path: '/about', name: 'about', component: About, meta: { showAbout: true } },
// { path: '/error', name: 'genericError',component: Errors, props: true }, { path: '/error', name: 'genericError', component: Errors, meta: { middlewares: [noEmptyError], err: null } },
// { path: '/404', name: '404',component: Errors, props: true }, // { path: '/404', name: '404',component: Errors, props: true },
// { path: '*', redirect: { name: '404' } }, // { path: '*', redirect: { name: '404' } },

View File

@ -0,0 +1,8 @@
export default function noEmptyError({ to, next }) {
next()
if (to.params.err == undefined) {
// return to home if no err object is provided to prevent an empty error message
next({ name: 'accounts' });
}
else next()
}

67
resources/js_vue3/stores/notify.js vendored Normal file
View File

@ -0,0 +1,67 @@
import { defineStore } from 'pinia'
import router from '@/router'
const { notify } = useNotification()
export const useNotifyStore = defineStore({
id: 'notify',
state: () => {
return {
err: null,
message: null,
originalMessage: null,
debug: null,
}
},
getters: {
},
actions: {
parseError(err) {
this.$reset
this.err = err
// Hnalde axios response error
if (err.response) {
if (err.response.status === 407) {
this.message = trans('errors.auth_proxy_failed'),
this.originalMessage = trans('errors.auth_proxy_failed_legend')
}
else if (err.response.status === 403) {
this.message = trans('errors.unauthorized'),
this.originalMessage = trans('errors.unauthorized_legend')
}
else if(err.response.data) {
this.message = err.response.data.message,
this.originalMessage = err.response.data.originalMessage ?? null
this.debug = err.response.data.debug ?? null
}
} else {
this.message = err.message
this.debug = err.stack ?? null
}
// else if (err.request) {
//
},
error(err) {
this.parseError(err)
router.push({ name: 'genericError' })
},
info(notification) {
notify({ type: 'is-success', ...notification})
},
warn(notification) {
notify({ type: 'is-warning', ...notification})
},
alert(notification) {
notify({ type: 'is-danger', ...notification})
},
},
})

View File

@ -0,0 +1,41 @@
<script setup>
import { useNotifyStore } from '@/stores/notify'
const errorHandler = useNotifyStore()
const router = useRouter()
const route = useRoute()
const showModal = true
const showDebug = computed(() => process.env.NODE_ENV === 'development')
const props = defineProps({
closable: {
type: Boolean,
default: true
}
})
function exit() {
window.history.length > 1 && route.name !== '404' ? router.go(-1) : router.push({ name: 'accounts' })
}
</script>
<template>
<div class="error-message">
<modal v-model="showModal" :closable="props.closable" @modal-closed="exit">
<div class="error-message" v-if="$route.name == '404'">
<p class="error-404"></p>
<p>{{ $t('errors.resource_not_found') }}</p>
</div>
<div v-else>
<p class="error-generic"></p>
<p>{{ $t('errors.error_occured') }} </p>
<p v-if="errorHandler.message" class="has-text-grey-lighter">{{ errorHandler.message }}</p>
<p v-if="errorHandler.originalMessage" class="has-text-grey-lighter">{{ errorHandler.originalMessage }}</p>
<p v-if="showDebug && errorHandler.debug" class="is-size-7 is-family-code"><br>{{ errorHandler.debug }}
</p>
</div>
</modal>
</div>
</template>

View File

@ -4,15 +4,16 @@
// import { useStorage } from '@vueuse/core' // import { useStorage } from '@vueuse/core'
import Form from '@/components/formElements/Form' import Form from '@/components/formElements/Form'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { useNotifyStore } from '@/stores/notify'
import { useAppSettingsStore } from '@/stores/appSettings' import { useAppSettingsStore } from '@/stores/appSettings'
// import { useRouter } from 'vue-router'; // import { useRouter } from 'vue-router';
// import { useNotification } from "@kyvg/vue3-notification"; // import { useNotification } from "@kyvg/vue3-notification";
// import { trans } from 'laravel-vue-i18n'; // import { trans } from 'laravel-vue-i18n';
const $2fauth = inject('2fauth') const $2fauth = inject('2fauth')
const { notify } = useNotification()
const router = useRouter() const router = useRouter()
const user = useUserStore() const user = useUserStore()
const notify = useNotifyStore()
const appSettings = useAppSettingsStore() const appSettings = useAppSettingsStore()
const showWebauthnForm = user.preferences.useWebauthnOnly ? true : useStorage($2fauth.prefix + 'showWebauthnForm', true) const showWebauthnForm = user.preferences.useWebauthnOnly ? true : useStorage($2fauth.prefix + 'showWebauthnForm', true)
const form = reactive(new Form({ const form = reactive(new Form({
@ -43,10 +44,10 @@
}) })
.catch(error => { .catch(error => {
if( error.response.status === 401 ) { if( error.response.status === 401 ) {
notify({ type: 'is-danger', text: trans('auth.forms.authentication_failed'), duration:-1 }) notify.alert({text: trans('auth.forms.authentication_failed'), duration:5 })
} }
else if( error.response.status !== 422 ) { else if( error.response.status !== 422 ) {
router.push({ name: 'genericError', params: { err: error.response } }); notify.error(error)
} }
}); });
} }