From 2ec9c210151016469b245c8733803a0b47725198 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 2 May 2026 14:14:00 +0200 Subject: [PATCH] refactor(router): align all 8 apps with Vue Router 4 best practices MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/accounting-app/app.ts | 24 +++++---------- src/activities-app/app.ts | 21 +++++--------- src/app.ts | 25 ++++++++-------- src/chat-app/app.ts | 22 +++++--------- src/forum-app/app.ts | 19 +++++------- src/lib/router-helpers.ts | 61 +++++++++++++++++++++++++++++++++++++++ src/market-app/app.ts | 19 +++++------- src/tasks-app/app.ts | 19 +++++------- src/wallet-app/app.ts | 24 +++++++-------- 9 files changed, 127 insertions(+), 107 deletions(-) create mode 100644 src/lib/router-helpers.ts diff --git a/src/accounting-app/app.ts b/src/accounting-app/app.ts index 38d7d99..704dbab 100644 --- a/src/accounting-app/app.ts +++ b/src/accounting-app/app.ts @@ -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) => { diff --git a/src/activities-app/app.ts b/src/activities-app/app.ts index 918ed38..e2bc818 100644 --- a/src/activities-app/app.ts +++ b/src/activities-app/app.ts @@ -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) => { diff --git a/src/app.ts b/src/app.ts index ebccb5c..73fc5b2 100644 --- a/src/app.ts +++ b/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') diff --git a/src/chat-app/app.ts b/src/chat-app/app.ts index c058f86..f107399 100644 --- a/src/chat-app/app.ts +++ b/src/chat-app/app.ts @@ -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) diff --git a/src/forum-app/app.ts b/src/forum-app/app.ts index 55d25ad..66d6d19 100644 --- a/src/forum-app/app.ts +++ b/src/forum-app/app.ts @@ -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) diff --git a/src/lib/router-helpers.ts b/src/lib/router-helpers.ts new file mode 100644 index 0000000..b0cac33 --- /dev/null +++ b/src/lib/router-helpers.ts @@ -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 = 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: '/', +} diff --git a/src/market-app/app.ts b/src/market-app/app.ts index bdeab91..9b7e936 100644 --- a/src/market-app/app.ts +++ b/src/market-app/app.ts @@ -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) diff --git a/src/tasks-app/app.ts b/src/tasks-app/app.ts index 004171c..25fce53 100644 --- a/src/tasks-app/app.ts +++ b/src/tasks-app/app.ts @@ -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) diff --git a/src/wallet-app/app.ts b/src/wallet-app/app.ts index 5380484..07d1cef 100644 --- a/src/wallet-app/app.ts +++ b/src/wallet-app/app.ts @@ -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)