import { createRouter, createWebHistory } from "vue-router"; import { useSessionStore } from "../stores/session"; import { useConfigStore } from "../stores/config"; import { isDesktop, logEvent } from "../desktop/bridge"; // One-time boot diagnostic: the first navigation is where config + session resolve, // so it's the moment that tells us whether the app got past its startup gate. let bootLogged = false; const router = createRouter({ history: createWebHistory(), routes: [ { // Persistent authed shell (sidebar + top bar + search); children render in it. path: "/", component: () => import("../components/AppShell.vue"), meta: { requiresAuth: true }, children: [ { path: "", name: "board", component: () => import("../views/BoardView.vue") }, { path: "archive", name: "archive", component: () => import("../views/BoardView.vue") }, { path: "trash", name: "trash", component: () => import("../views/BoardView.vue") }, { path: "label/:id", name: "label", component: () => import("../views/BoardView.vue") }, { path: "search", name: "search", component: () => import("../views/SearchView.vue") }, { path: "graph", name: "graph", component: () => import("../views/GraphView.vue") }, { path: "reminders", name: "reminders", component: () => import("../views/RemindersView.vue") }, { path: "timeline", name: "timeline", component: () => import("../views/TimelineView.vue") }, ], }, { path: "/settings", name: "settings", component: () => import("../views/SettingsView.vue"), meta: { requiresAuth: true, requiresAdmin: true }, }, { // Desktop only: connect this app to a server. Meaningless in the web build, // which IS a server's UI — there's nothing for it to link to. path: "/sync", name: "sync", component: () => import("../views/SyncView.vue"), meta: { requiresAuth: true, requiresDesktop: true }, }, { // Per-user account: linked devices (native-client sync tokens). Any user. // // The mirror of `requiresDesktop` above: this one needs a SERVER. The desktop // is itself one of the devices this page lists, so offline the list is always // empty and issuing a token rejects — its server relationship lives at /sync. // Guarded in the router, not just hidden in the shell, so a typed URL or a // restored history entry can't land on a dead end either. path: "/account", name: "account", component: () => import("../views/AccountView.vue"), meta: { requiresAuth: true, requiresServer: true }, }, { path: "/login", name: "login", component: () => import("../views/LoginView.vue"), meta: { guestOnly: true }, }, { path: "/register", name: "register", component: () => import("../views/RegisterView.vue"), meta: { guestOnly: true }, }, ], }); router.beforeEach(async (to) => { const session = useSessionStore(); const config = useConfigStore(); await config.load(); if (!session.loaded) { await session.fetchMe(); } if (!bootLogged) { bootLogged = true; logEvent( "info", `first route: config(site=${config.siteName}) session(user=${session.user?.email ?? "none"}) -> ${String(to.name ?? to.path)}`, ); } if (to.meta.requiresAuth && !session.user) { return { name: "login", query: to.fullPath !== "/" ? { redirect: to.fullPath } : undefined }; } if (to.meta.requiresAdmin && !session.user?.is_admin) { return { name: "board" }; } if (to.meta.requiresDesktop && !isDesktop()) { return { name: "board" }; } // Deliberately NOT applied to /login and /register: bouncing those on desktop // would loop against the requiresAuth guard above the moment a session is // missing. Nothing on the desktop navigates to them any more (AppShell's sign-out // is web-only), and a fresh launch always resolves the local user. if (to.meta.requiresServer && isDesktop()) { return { name: "board" }; } if (to.name === "register" && !config.allowRegistration) { return { name: "login" }; } if (to.meta.guestOnly && session.user) { return { name: "board" }; } return true; }); export default router;