refactor(router): align all 8 apps with Vue Router 4 best practices
Centralizes guard installation in src/lib/router-helpers.ts and
applies five docs-recommended patterns uniformly across hub, wallet,
chat, market, tasks, forum, castle, and sortir.
1. Guard registration order
Vue Router docs: install guards before app.use(router). Was: each
app installed beforeEach() at the very end of createAppInstance(),
long after app.use(router) and after auth.initialize(). Worked
because mount happens last, but fragile.
Now: installLenientAuthGuard()/installStrictAuthGuard() runs
immediately after createRouter(), before app.use(router).
2. Return-based guard signatures
Vue Router 4 docs prefer returning a route location over the
next() callback (easier to misuse — forgot next() = hung
navigation, called twice = warning). Both helpers return paths
('/login', '/') or true to allow.
3. Removed misleading async on guards with no await
The old guards declared async (to, _from, next) => {...} but
never awaited anything. The new guards are genuinely async (they
await auth-readiness) so the async is justified.
4. Catch-all 404 route
Each router now ends with catchAllRoute = { path:
'/:pathMatch(.*)*', redirect: '/' }. Vue Router warns at runtime
if no catch-all is defined.
5. Auth-readiness deferred promise
Auth depends on services registered during
pluginManager.installAll() so it can't be imported at the top of
each app.ts. The helper exposes markAuthReady(auth) which
resolves a module-level promise; guards await this promise on
first invocation. Resolves the chicken-and-egg between
"guards-before-router" (Vue Router docs) and
"auth-after-services" (our DI lifecycle). Each app calls
markAuthReady() right after auth.initialize() succeeds.
Strict (wallet, chat, castle): every non-/login route requires auth.
Lenient (hub, forum, market, tasks, activities): only routes with
meta.requiresAuth === true are gated.
Behavior is unchanged from commit 4605703 — this is a refactor.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3ec66151a7
commit
2ec9c21015
9 changed files with 127 additions and 107 deletions
|
|
@ -14,6 +14,7 @@ import App from './App.vue'
|
|||
|
||||
import '@/assets/index.css'
|
||||
import { i18n, changeLocale, type AvailableLocale } from '@/i18n'
|
||||
import { installStrictAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers'
|
||||
|
||||
/**
|
||||
* Accept an auth token from a URL parameter (e.g. ?token=xxx).
|
||||
|
|
@ -89,9 +90,13 @@ export async function createAppInstance() {
|
|||
component: () => import('./views/SettingsPage.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
catchAllRoute,
|
||||
]
|
||||
})
|
||||
|
||||
// Castle has no public view — every non-login route requires auth.
|
||||
installStrictAuthGuard(router)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
|
|
@ -131,24 +136,11 @@ export async function createAppInstance() {
|
|||
await Promise.all(moduleRegistrations)
|
||||
await pluginManager.installAll()
|
||||
|
||||
// Initialize auth
|
||||
// Dynamic import: useAuthService depends on services registered by
|
||||
// pluginManager.installAll() (LNbits API).
|
||||
const { auth } = await import('@/composables/useAuthService')
|
||||
await auth.initialize()
|
||||
|
||||
// Auth guard — only redirect for routes that explicitly require auth
|
||||
// Castle has no public view — every non-login route requires auth.
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
if (to.path === '/login') {
|
||||
if (auth.isAuthenticated.value) next('/')
|
||||
else next()
|
||||
return
|
||||
}
|
||||
if (!auth.isAuthenticated.value) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
next()
|
||||
})
|
||||
markAuthReady(auth)
|
||||
|
||||
// Global error handling
|
||||
app.config.errorHandler = (err, _vm, info) => {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import App from './App.vue'
|
|||
|
||||
import '@/assets/index.css'
|
||||
import { i18n, changeLocale, type AvailableLocale } from '@/i18n'
|
||||
import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers'
|
||||
|
||||
/**
|
||||
* Accept an auth token from a URL parameter (e.g. ?token=xxx).
|
||||
|
|
@ -73,9 +74,12 @@ export async function createAppInstance() {
|
|||
component: () => import('./views/SettingsPage.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
catchAllRoute,
|
||||
]
|
||||
})
|
||||
|
||||
installLenientAuthGuard(router)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
|
|
@ -109,22 +113,11 @@ export async function createAppInstance() {
|
|||
await Promise.all(moduleRegistrations)
|
||||
await pluginManager.installAll()
|
||||
|
||||
// Initialize auth
|
||||
// Dynamic import: useAuthService depends on services registered by
|
||||
// pluginManager.installAll() (LNbits API).
|
||||
const { auth } = await import('@/composables/useAuthService')
|
||||
await auth.initialize()
|
||||
|
||||
// Auth guard — only redirect for routes that explicitly require auth
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const requiresAuth = to.meta.requiresAuth === true
|
||||
|
||||
if (requiresAuth && !auth.isAuthenticated.value) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && auth.isAuthenticated.value) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
markAuthReady(auth)
|
||||
|
||||
// Global error handling
|
||||
app.config.errorHandler = (err, _vm, info) => {
|
||||
|
|
|
|||
25
src/app.ts
25
src/app.ts
|
|
@ -13,6 +13,7 @@ import App from './App.vue'
|
|||
|
||||
import './assets/index.css'
|
||||
import { i18n } from './i18n'
|
||||
import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers'
|
||||
|
||||
/**
|
||||
* Initialize and start the minimal AIO hub.
|
||||
|
|
@ -47,10 +48,15 @@ export async function createAppInstance() {
|
|||
: () => import('./pages/Login.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
...moduleRoutes
|
||||
...moduleRoutes,
|
||||
catchAllRoute,
|
||||
]
|
||||
})
|
||||
|
||||
// Register guards immediately (Vue Router docs: before app.use(router)).
|
||||
// Guards await auth readiness internally — see router-helpers.ts.
|
||||
installLenientAuthGuard(router)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
|
|
@ -70,22 +76,15 @@ export async function createAppInstance() {
|
|||
await Promise.all(moduleRegistrations)
|
||||
await pluginManager.installAll()
|
||||
|
||||
// Dynamic import: useAuthService depends on services registered by
|
||||
// pluginManager.installAll() (LNbits API) so it can't be imported at
|
||||
// the top of this file. Once initialized, we signal the router-guard
|
||||
// promise so any pending navigations can resolve.
|
||||
const { auth } = await import('@/composables/useAuthService')
|
||||
await auth.initialize()
|
||||
markAuthReady(auth)
|
||||
console.log('Auth initialized, isAuthenticated:', auth.isAuthenticated.value)
|
||||
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const requiresAuth = to.meta.requiresAuth === true
|
||||
|
||||
if (requiresAuth && !auth.isAuthenticated.value) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && auth.isAuthenticated.value) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
app.config.errorHandler = (err, _vm, info) => {
|
||||
console.error('Global error:', err, info)
|
||||
eventBus.emit('app:error', { error: err, info }, 'app')
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import App from './App.vue'
|
|||
|
||||
import '@/assets/index.css'
|
||||
import { i18n, changeLocale, type AvailableLocale } from '@/i18n'
|
||||
import { installStrictAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers'
|
||||
|
||||
function acceptTokenFromUrl() {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
|
@ -55,9 +56,13 @@ export async function createAppInstance() {
|
|||
meta: { requiresAuth: false }
|
||||
},
|
||||
...moduleRoutes,
|
||||
catchAllRoute,
|
||||
]
|
||||
})
|
||||
|
||||
// Chat has no public view — every non-login route requires auth.
|
||||
installStrictAuthGuard(router)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
|
|
@ -88,22 +93,11 @@ export async function createAppInstance() {
|
|||
await Promise.all(moduleRegistrations)
|
||||
await pluginManager.installAll()
|
||||
|
||||
// Dynamic import: useAuthService depends on services registered by
|
||||
// pluginManager.installAll() (LNbits API).
|
||||
const { auth } = await import('@/composables/useAuthService')
|
||||
await auth.initialize()
|
||||
|
||||
// Chat has no public view — every non-login route requires auth.
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
if (to.path === '/login') {
|
||||
if (auth.isAuthenticated.value) next('/')
|
||||
else next()
|
||||
return
|
||||
}
|
||||
if (!auth.isAuthenticated.value) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
next()
|
||||
})
|
||||
markAuthReady(auth)
|
||||
|
||||
app.config.errorHandler = (err, _vm, info) => {
|
||||
console.error('Global error:', err, info)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import App from './App.vue'
|
|||
|
||||
import '@/assets/index.css'
|
||||
import { i18n, changeLocale, type AvailableLocale } from '@/i18n'
|
||||
import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers'
|
||||
|
||||
function acceptTokenFromUrl() {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
|
@ -55,9 +56,12 @@ export async function createAppInstance() {
|
|||
meta: { requiresAuth: false }
|
||||
},
|
||||
...moduleRoutes,
|
||||
catchAllRoute,
|
||||
]
|
||||
})
|
||||
|
||||
installLenientAuthGuard(router)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
|
|
@ -88,20 +92,11 @@ export async function createAppInstance() {
|
|||
await Promise.all(moduleRegistrations)
|
||||
await pluginManager.installAll()
|
||||
|
||||
// Dynamic import: useAuthService depends on services registered by
|
||||
// pluginManager.installAll() (LNbits API).
|
||||
const { auth } = await import('@/composables/useAuthService')
|
||||
await auth.initialize()
|
||||
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const requiresAuth = to.meta.requiresAuth === true
|
||||
|
||||
if (requiresAuth && !auth.isAuthenticated.value) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && auth.isAuthenticated.value) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
markAuthReady(auth)
|
||||
|
||||
app.config.errorHandler = (err, _vm, info) => {
|
||||
console.error('Global error:', err, info)
|
||||
|
|
|
|||
61
src/lib/router-helpers.ts
Normal file
61
src/lib/router-helpers.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import type { Router, RouteRecordRaw } from 'vue-router'
|
||||
|
||||
/**
|
||||
* Auth-readiness deferred promise.
|
||||
*
|
||||
* Each app boots in three phases:
|
||||
* 1. createRouter(...) and install guards (this file)
|
||||
* 2. pluginManager.installAll() registers services (incl. LNbits API)
|
||||
* 3. dynamic-import('@/composables/useAuthService') and auth.initialize()
|
||||
*
|
||||
* The auth service depends on services registered in phase 2, so it can only
|
||||
* be loaded after that completes. But Vue Router's docs recommend installing
|
||||
* guards before app.use(router). The deferred promise resolves the order
|
||||
* mismatch: guards register early but await this promise before reading
|
||||
* auth state. Phase 3 calls markAuthReady() once auth is initialized.
|
||||
*/
|
||||
type AuthLike = { isAuthenticated: { value: boolean } }
|
||||
|
||||
let resolveAuth!: (a: AuthLike) => void
|
||||
const authReady: Promise<AuthLike> = new Promise(r => { resolveAuth = r })
|
||||
|
||||
export function markAuthReady(auth: AuthLike): void {
|
||||
resolveAuth(auth)
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict guard — every non-/login route requires auth.
|
||||
* Used by wallet, chat, castle (no public view).
|
||||
*/
|
||||
export function installStrictAuthGuard(router: Router): void {
|
||||
router.beforeEach(async (to) => {
|
||||
const auth = await authReady
|
||||
if (to.path === '/login') {
|
||||
return auth.isAuthenticated.value ? '/' : true
|
||||
}
|
||||
return auth.isAuthenticated.value ? true : '/login'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Lenient guard — only routes with meta.requiresAuth === true require auth.
|
||||
* Used by hub and the public standalones (forum, market, tasks, activities).
|
||||
*/
|
||||
export function installLenientAuthGuard(router: Router): void {
|
||||
router.beforeEach(async (to) => {
|
||||
const auth = await authReady
|
||||
const requiresAuth = to.meta.requiresAuth === true
|
||||
if (requiresAuth && !auth.isAuthenticated.value) return '/login'
|
||||
if (to.path === '/login' && auth.isAuthenticated.value) return '/'
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch-all 404 → redirect home. Add as the LAST entry in any router's
|
||||
* routes array. Vue Router 4 warns if no catch-all is defined.
|
||||
*/
|
||||
export const catchAllRoute: RouteRecordRaw = {
|
||||
path: '/:pathMatch(.*)*',
|
||||
redirect: '/',
|
||||
}
|
||||
|
|
@ -13,6 +13,7 @@ import App from './App.vue'
|
|||
|
||||
import '@/assets/index.css'
|
||||
import { i18n, changeLocale, type AvailableLocale } from '@/i18n'
|
||||
import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers'
|
||||
|
||||
function acceptTokenFromUrl() {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
|
@ -55,9 +56,12 @@ export async function createAppInstance() {
|
|||
meta: { requiresAuth: false }
|
||||
},
|
||||
...moduleRoutes,
|
||||
catchAllRoute,
|
||||
]
|
||||
})
|
||||
|
||||
installLenientAuthGuard(router)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
|
|
@ -88,20 +92,11 @@ export async function createAppInstance() {
|
|||
await Promise.all(moduleRegistrations)
|
||||
await pluginManager.installAll()
|
||||
|
||||
// Dynamic import: useAuthService depends on services registered by
|
||||
// pluginManager.installAll() (LNbits API).
|
||||
const { auth } = await import('@/composables/useAuthService')
|
||||
await auth.initialize()
|
||||
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const requiresAuth = to.meta.requiresAuth === true
|
||||
|
||||
if (requiresAuth && !auth.isAuthenticated.value) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && auth.isAuthenticated.value) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
markAuthReady(auth)
|
||||
|
||||
app.config.errorHandler = (err, _vm, info) => {
|
||||
console.error('Global error:', err, info)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import App from './App.vue'
|
|||
|
||||
import '@/assets/index.css'
|
||||
import { i18n, changeLocale, type AvailableLocale } from '@/i18n'
|
||||
import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers'
|
||||
|
||||
function acceptTokenFromUrl() {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
|
|
@ -55,9 +56,12 @@ export async function createAppInstance() {
|
|||
meta: { requiresAuth: false }
|
||||
},
|
||||
...moduleRoutes,
|
||||
catchAllRoute,
|
||||
]
|
||||
})
|
||||
|
||||
installLenientAuthGuard(router)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
|
|
@ -88,20 +92,11 @@ export async function createAppInstance() {
|
|||
await Promise.all(moduleRegistrations)
|
||||
await pluginManager.installAll()
|
||||
|
||||
// Dynamic import: useAuthService depends on services registered by
|
||||
// pluginManager.installAll() (LNbits API).
|
||||
const { auth } = await import('@/composables/useAuthService')
|
||||
await auth.initialize()
|
||||
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
const requiresAuth = to.meta.requiresAuth === true
|
||||
|
||||
if (requiresAuth && !auth.isAuthenticated.value) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && auth.isAuthenticated.value) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
markAuthReady(auth)
|
||||
|
||||
app.config.errorHandler = (err, _vm, info) => {
|
||||
console.error('Global error:', err, info)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import App from './App.vue'
|
|||
|
||||
import '@/assets/index.css'
|
||||
import { i18n, changeLocale, type AvailableLocale } from '@/i18n'
|
||||
import { installStrictAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers'
|
||||
|
||||
/**
|
||||
* Accept an auth token from a URL parameter (e.g. ?token=xxx).
|
||||
|
|
@ -59,9 +60,15 @@ export async function createAppInstance() {
|
|||
meta: { requiresAuth: false }
|
||||
},
|
||||
...moduleRoutes,
|
||||
catchAllRoute,
|
||||
]
|
||||
})
|
||||
|
||||
// Wallet has no public view — every non-login route requires auth.
|
||||
// Guard is installed before app.use(router); it awaits auth readiness
|
||||
// internally (see router-helpers.ts).
|
||||
installStrictAuthGuard(router)
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(router)
|
||||
|
|
@ -92,22 +99,11 @@ export async function createAppInstance() {
|
|||
await Promise.all(moduleRegistrations)
|
||||
await pluginManager.installAll()
|
||||
|
||||
// Dynamic import: useAuthService depends on services registered by
|
||||
// pluginManager.installAll() (LNbits API).
|
||||
const { auth } = await import('@/composables/useAuthService')
|
||||
await auth.initialize()
|
||||
|
||||
// Wallet has no public view — every non-login route requires auth.
|
||||
router.beforeEach(async (to, _from, next) => {
|
||||
if (to.path === '/login') {
|
||||
if (auth.isAuthenticated.value) next('/')
|
||||
else next()
|
||||
return
|
||||
}
|
||||
if (!auth.isAuthenticated.value) {
|
||||
next('/login')
|
||||
return
|
||||
}
|
||||
next()
|
||||
})
|
||||
markAuthReady(auth)
|
||||
|
||||
app.config.errorHandler = (err, _vm, info) => {
|
||||
console.error('Global error:', err, info)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue