mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-07-19 07:15:20 +00:00
fix(auth): redirect to login on 401 instead of hanging on bootstrap
When the Sanctum session/token expires, /api/v1/bootstrap returns 401 Unauthenticated, CompanyLayout's initializeLayout() throws, and isAppLoaded stays false — leaving the user on a spinning loader with no way out but a hard refresh to /login. Adds a response interceptor to the main axios client that catches any 401, clears stale auth state (auth.token, selectedCompany, isAdminMode), and navigates to /login?next=<original-path> so the user lands back where they were after re-auth. Exempts /login, /logout, /sanctum/csrf- cookie (where a 401 is a legitimate form/flow signal, not a session expiry), and guards against redirect loops via a module-level flag that collapses concurrent 401s into a single navigation. Also bails out on the login route itself, on /installation, and on customer- portal routes (which already have their own handling in the router guard). LoginView reads the ?next query param on successful login (sanitized to same-origin paths only, rejecting protocol-relative and absolute URLs so a crafted link can never open-redirect) and redirects there, falling back to /admin/dashboard. The router is imported dynamically inside the interceptor to break the client → router → guards → stores → client circular that a top-level import would create. Vite bundles the dynamic import into the main chunk, so it's free at runtime.
This commit is contained in:
@@ -58,7 +58,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { required, email, helpers } from '@vuelidate/validators'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
@@ -75,6 +75,7 @@ const notificationStore = useNotificationStore()
|
||||
const authStore = useAuthStore()
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const isLoading = ref<boolean>(false)
|
||||
const isShowPassword = ref<boolean>(false)
|
||||
|
||||
@@ -109,7 +110,18 @@ async function onSubmit(): Promise<void> {
|
||||
try {
|
||||
await authStore.login(authStore.loginData)
|
||||
|
||||
router.push('/admin/dashboard')
|
||||
// Honour a ?next= query param if present (set by the 401 response
|
||||
// interceptor so users land back on the page they were trying to
|
||||
// reach). Sanitize to same-origin paths only — reject protocol-
|
||||
// relative (`//evil.com`) and absolute URLs so a crafted link
|
||||
// can never open-redirect.
|
||||
const nextRaw = typeof route.query.next === 'string' ? route.query.next : ''
|
||||
const safeNext =
|
||||
nextRaw.startsWith('/') && !nextRaw.startsWith('//')
|
||||
? nextRaw
|
||||
: '/admin/dashboard'
|
||||
|
||||
router.push(safeNext)
|
||||
|
||||
notificationStore.showNotification({
|
||||
type: 'success',
|
||||
|
||||
Reference in New Issue
Block a user