Wire Pinia auth store + Vue Router beforeEach guard + isMock auto-login + localStorage token persistence, matching the admin-dashboard/employee-portal pattern.
Trigger when the user asks to:
useAuthStore, defineStore('auth', ...), router.beforeEach, meta.public, VITE_MOCKBoth admin-dashboard and employee-portal ship the SAME minimal auth stack:
src/stores/auth.ts exposing token (persisted in localStorage under admin_token / portal_token), user, isAuthenticated computed, login(username, password), logout().src/router/index.ts reading import.meta.env.VITE_MOCK === 'true' and auto-calling when in mock mode + not yet authenticated.auth.login('admin', 'admin')meta: { public: true } on the Login route only — every other route requires auth.api/real-client.ts) attaches Authorization: Bearer ${token} via an axios interceptor reading the same localStorage key.The mock-mode auto-login is the demo gate: pnpm dev with VITE_MOCK=true skips the login form entirely so reviewers land on the dashboard directly.
src/stores/auth.ts, create it copying admin-dashboard/src/stores/auth.ts:1-23 verbatim and rename the localStorage key to <project>_token.src/router/index.ts and ensure ONLY the /login route has meta: { public: true }.beforeEach guard reading isMock and auto-login (lines 52-62 of admin-dashboard).src/api/real-client.ts to read the same localStorage key and attach the bearer header.routes: [...] — the default guard will protect them. NEVER add meta: { public: true } unless the route is the login page itself.meta with meta: { roles: ['Admin'] } and check auth.user?.role inside the guard — but only add this if the user actually requires RBAC; default is auth-only.Pinia store (copy verbatim, rename localStorage key per project):
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('admin_token') || '')
const user = ref<{ name: string; role: string } | null>(null)
const isAuthenticated = computed(() => !!token.value)
function login(username: string, _password: string) {
token.value = 'mock-jwt-token'
user.value = { name: username, role: 'Admin' }
localStorage.setItem('admin_token', token.value)
}
function logout() {
token.value = ''
user.value = null
localStorage.removeItem('admin_token')
}
return { token, user, isAuthenticated, login, logout }
})
Router guard (must include the isMock auto-login branch):
const isMock = import.meta.env.VITE_MOCK === 'true'
router.beforeEach(async (to) => {
const auth = useAuthStore()
if (isMock && !auth.isAuthenticated) {
await auth.login('admin', 'admin')
}
if (!to.meta.public && !auth.isAuthenticated) {
return '/login'
}
})
Axios interceptor (in real-client.ts):
http.interceptors.request.use((config) => {
const token = localStorage.getItem('admin_token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
isMock auto-login branch — without it, reviewers running the demo see a login wall instead of the app.sessionStorage or in-memory — the project relies on localStorage so reloads keep state.onMounted redirects to /login; the router guard owns that responsibility.localStorage.meta to meta: { public: true } — the guard contract is "auth required by default".admin-dashboard/src/stores/auth.ts:1-23 — full Pinia storeadmin-dashboard/src/router/index.ts:52-62 — beforeEach with isMock auto-loginadmin-dashboard/src/api/real-client.ts:9-13 — axios bearer interceptoremployee-portal/src/stores/auth.ts — sibling implementation (same shape, different storage key)