Skip to content

Route Guards

12.2.1. Global Guards

  • Global guards must be registered using router.beforeEach.
  • In Vue Router 4, return values must be used instead of the next() callback. next() must not be used.
  • To allow navigation, return true or undefined.
  • To block navigation, return false or a redirect route.
typescript
// src/router/index.ts
router.beforeEach((to, from) => {
  // Allow pages that do not require authentication
  if (!to.meta.requiresAuth) {
    return true
  }

  const authStore = useAuthStore()

  if (!authStore.isAuthenticated) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }

  return true
})
  • router.afterEach is used for setting page titles, sending analytics events, and similar tasks.
typescript
router.afterEach((to) => {
  document.title = `${to.meta.title} | TQS`
})

12.2.2. Per-Route Guards

  • Guards that apply only to specific routes must use the beforeEnter property.
  • Role-based access control must be implemented using per-route guards in conjunction with meta.roles.
  • When guard logic becomes complex, it should be extracted into a separate function.
typescript
// src/router/guards/roleGuard.ts
import type { NavigationGuardWithThis } from 'vue-router'
import { useAuthStore } from '@/stores/useAuthStore'

export function requireRoles(
  allowedRoles: string[]
): NavigationGuardWithThis<undefined> {
  return () => {
    const authStore = useAuthStore()
    const userRole = authStore.user?.role

    if (!userRole || !allowedRoles.includes(userRole)) {
      return { name: 'Forbidden' }
    }

    return true
  }
}
typescript
// src/router/routes.ts
import { requireRoles } from './guards/roleGuard'

{
  path: '/admin/dashboard',
  name: 'AdminDashboard',
  component: () => import('@/views/admin/DashboardView.vue'),
  beforeEnter: requireRoles(['admin', 'super-admin']),
  meta: { title: 'Admin Dashboard', requiresAuth: true, roles: ['admin'] },
}

12.2.3. In-Component Guards

  • In-component guards must use the Composition API's onBeforeRouteLeave and onBeforeRouteUpdate.
  • The Options API's beforeRouteEnter and beforeRouteLeave must not be used.
  • onBeforeRouteLeave is used to warn users about unsaved data when leaving a page.
vue
<script setup lang="ts">
import { ref } from 'vue'
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

const hasUnsavedChanges = ref(false)

onBeforeRouteLeave(() => {
  if (hasUnsavedChanges.value) {
    const answer = window.confirm('You have unsaved changes. Do you want to leave?')
    if (!answer) {
      return false
    }
  }
})

onBeforeRouteUpdate(async (to) => {
  // Reload data when only the parameters change within the same component
  await fetchData(to.params.id as string)
})
</script>

12.2.4. Authentication Guard Implementation

  • The authentication guard must operate in the following sequence.
StepActionOn Failure
1Check token existenceRedirect to login page
2Validate tokenAttempt token refresh
3Load user informationRedirect to login page
4Verify permissionsRedirect to 403 page
typescript
// src/router/guards/authGuard.ts
import { useAuthStore } from '@/stores/useAuthStore'

export async function authGuard(to: RouteLocationNormalized) {
  if (!to.meta.requiresAuth) {
    return true
  }

  const authStore = useAuthStore()

  // Step 1: Check token
  if (!authStore.token) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }

  // Step 2: Validate and refresh token
  if (authStore.isTokenExpired) {
    const refreshed = await authStore.refreshToken()
    if (!refreshed) {
      authStore.clearAuth()
      return { name: 'Login', query: { redirect: to.fullPath } }
    }
  }

  // Step 3: Load user information
  if (!authStore.user) {
    try {
      await authStore.fetchUser()
    } catch {
      authStore.clearAuth()
      return { name: 'Login', query: { redirect: to.fullPath } }
    }
  }

  // Step 4: Verify permissions
  const requiredRoles = to.meta.roles
  if (requiredRoles && !requiredRoles.includes(authStore.user.role)) {
    return { name: 'Forbidden' }
  }

  return true
}
typescript
// src/router/index.ts
import { authGuard } from './guards/authGuard'

router.beforeEach(authGuard)
  • The authentication guard must be registered as a single entry point in the global beforeEach.
  • When redirecting to the login page, the original path must be preserved in query.redirect.
  • On token refresh failure, all stored authentication information must be cleared.

TIENIPIA QUALIFIED STANDARD