# How the Permission Guard System Operates in the Celeris Web Router

> Learn how the Celeris Web permission guard system intercepts navigation, validates tokens, checks whitelists, and injects routes before allowing router resolution. Explore the kirklin/celeris-web repository.

- Repository: [Kirk Lin/celeris-web](https://github.com/kirklin/celeris-web)
- Tags: internals
- Published: 2026-03-05

---

**The Celeris Web permission guard system intercepts every navigation event through a global `beforeEach` hook that validates authentication tokens, checks route whitelists, and dynamically injects authorized routes before allowing the router to resolve the destination.**

The Celeris Web frontend implements a robust access-control layer directly within its Vue Router configuration, ensuring that navigation permissions are enforced before any page renders. By centralizing authorization logic in the `createPermissionGuard` function, the system separates routing concerns from authentication state management while supporting multiple permission modes. This analysis examines the exact source code implementation in the `kirklin/celeris-web` repository to demonstrate how the guard orchestrates token validation, dynamic route generation, and state cleanup.

## Core Architecture and Guard Registration

The permission guard is wired into the application during router initialization. In [`apps/admin/src/router/index.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/router/index.ts), the router instance is created with static public routes, then immediately passed to `setupRouterGuards` to register global navigation hooks.

```typescript
// apps/admin/src/router/index.ts
import { createRouter, createWebHistory } from "vue-router"
import { setupRouterGuards } from "~/router/guard"
import { staticRoutes } from "~/router/routes"

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: staticRoutes,
})

// Register global guards (permission + state cleanup)
setupRouterGuards(router)
export default router

```

The `setupRouterGuards` function in [`apps/admin/src/router/guard/index.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/router/guard/index.ts) instantiates both the permission validator and a separate state-management guard.

```typescript
// apps/admin/src/router/guard/index.ts
import { createPermissionGuard } from "~/router/guard/permissionGuard"
import { createStateGuard } from "~/router/guard/stateGuard"

export const setupRouterGuards = (router: Router) => {
  createPermissionGuard(router)  // Runs first
  createStateGuard(router)      // Runs second
}

```

## Inside the Permission Guard Execution Flow

The `createPermissionGuard` function in [`apps/admin/src/router/guard/permissionGuard.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/router/guard/permissionGuard.ts) attaches an asynchronous `beforeEach` handler that executes the following evaluation steps on every navigation:

### 1. Whitelist Bypass for Public Pages

The guard maintains a `whitePathList` array that contains paths accessible without authentication (specifically the login page). If the target path is whitelisted, the guard checks whether the user already has a valid token. When an authenticated user attempts to visit the login page, they are redirected to their configured home URL instead of seeing the login form.

### 2. Token-Based Authentication Check

For non-whitelisted routes, the guard verifies the presence of a token via `userStore.getToken`. If no token exists and the route's metadata does not contain `shouldIgnoreAuth: true`, the navigation is aborted and redirected to the login page, preserving the intended destination in a query parameter.

### 3. Post-Login 404 Resolution

After authentication, if the router would land on the 404 *Page Not Found* route (which exists in the static route table), the guard automatically redirects to the user's home page to prevent confusing error screens immediately after login.

### 4. User Info Hydration

If the user store indicates it has never been initialized (`updatedAt === 0`), the guard calls `await userStore.getUserInfoAction()` to fetch profile data before proceeding. This ensures that downstream logic has access to role identifiers and permissions.

### 5. Dynamic Route Injection

The most critical operation occurs on the first visit to a protected route. The guard checks `permissionStore.getShouldAddRouteDynamically` to determine if routes have already been built. If not, it executes:

```typescript
const routes = await permissionStore.buildRoutesAction()
routes.forEach((route) => {
  router.addRoute(route)
})
router.addRoute(PAGE_NOT_FOUND_ROUTE as unknown as RouteRecordRaw)
permissionStore.setShouldAddRouteDynamically(true)

```

After injection, if the original target was the placeholder 404 route, the guard triggers a secondary navigation to the `fullPath`, allowing Vue Router to correctly match against the newly added dynamic routes.

## Dynamic Route Generation Based on Permission Mode

The `buildRoutesAction` method in [`apps/admin/src/store/modules/permission.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/modules/permission.ts) determines which routes to inject based on the application's configured permission strategy:

- **ROLE mode**: Generates routes from a static mapping filtered by the user's role ID.
- **ROUTE_MAPPING mode**: Fetches a simplified route list from the backend and maps it to Vue Router objects.
- **BACKEND mode**: Directly fetches complete route definitions from `/api/permission/routes` and returns them as `RouteRecordRaw` objects.

This architecture allows the same guard logic to support everything from simple role-based filtering to fully server-driven navigation trees without changing the router configuration.

## State Cleanup Between Navigations

A separate `createStateGuard` in [`apps/admin/src/router/guard/stateGuard.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/router/guard/stateGuard.ts) runs before each route change to prevent permission state from leaking between pages. When the path actually changes (not just query parameters), it resets both the permission and user stores:

```typescript
// apps/admin/src/router/guard/stateGuard.ts
export const createStateGuard = (router: Router) => {
  router.beforeEach((to, from, next) => {
    if (from.path !== to.path) {
      const permissionStore = usePermissionStore()
      const userStore = useUserStore()
      permissionStore.resetPermissionState()
      userStore.resetUserState()
    }
    next()
  })
}

```

This ensures that stale permission codes or user data do not persist when navigating to a new section of the application.

## Summary

- The permission guard is registered via `setupRouterGuards` immediately after router creation in [`apps/admin/src/router/index.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/router/index.ts).
- A whitelist check allows public pages (like login) to bypass authentication verification entirely.
- Unauthenticated users attempting to access protected routes are redirected to login with a preserved `redirect` query parameter.
- On first navigation to a protected area, `buildRoutesAction` generates routes based on the configured permission mode (ROLE, ROUTE_MAPPING, or BACKEND) and injects them using `router.addRoute`.
- A companion state guard resets Pinia stores whenever the navigation path changes, preventing data leakage between routes.
- All authorization logic is centralized in [`apps/admin/src/router/guard/permissionGuard.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/router/guard/permissionGuard.ts) while state management resides in [`apps/admin/src/store/modules/permission.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/modules/permission.ts) and [`apps/admin/src/store/modules/user.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/store/modules/user.ts).

## Frequently Asked Questions

### What happens if a user navigates to a protected route without an authentication token?

The guard detects the missing token via `userStore.getToken` and checks the route's `shouldIgnoreAuth` metadata. If the flag is not present, the navigation is cancelled and the user is redirected to the login page with a `redirect` query parameter containing the original target path, allowing seamless return after authentication.

### How does the system handle dynamic route generation for different user roles?

On the first protected navigation, the guard calls `permissionStore.buildRoutesAction()`, which inspects `appStore.getProjectSetting.permissionMode` to determine whether to generate routes from static role mappings, backend route mappings, or a direct API endpoint. The resulting `RouteRecordRaw` array is then injected into the router using `router.addRoute()` for each entry.

### Why does the permission guard redirect to the home page after login when landing on a 404 route?

Immediately after authentication, if the router resolves to the static 404 catch-all route (indicating the requested path does not exist in the static route table), the guard assumes the user attempted to deep-link to a dynamic route that hasn't been built yet. Rather than showing an error page, it redirects to the user's configured home URL to provide a graceful landing point.

### What is the purpose of the state guard alongside the permission guard?

While the permission guard handles authorization logic, the state guard in [`apps/admin/src/router/guard/stateGuard.ts`](https://github.com/kirklin/celeris-web/blob/main/apps/admin/src/router/guard/stateGuard.ts) focuses on data hygiene. It resets the permission and user Pinia stores whenever the navigation path changes, ensuring that permissions and user data from a previous session or page do not incorrectly influence the new page's behavior.