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:
Padreug 2026-05-02 14:14:00 +02:00
commit 2ec9c21015
9 changed files with 127 additions and 107 deletions

View file

@ -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) => {

View file

@ -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) => {

View file

@ -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')

View file

@ -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)

View file

@ -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
View 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: '/',
}

View file

@ -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)

View file

@ -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)

View file

@ -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)