2026-01-24 22:59:20 +00:00
|
|
|
import { createRouter, createWebHistory } from 'vue-router'
|
2026-02-17 19:19:54 +00:00
|
|
|
import { nextTick } from 'vue'
|
2026-01-24 22:59:20 +00:00
|
|
|
import { useAppStore } from '../stores/app'
|
2026-03-12 00:19:30 +00:00
|
|
|
import { stopAllAudio } from '../composables/useLoginSounds'
|
2026-01-24 22:59:20 +00:00
|
|
|
|
|
|
|
|
const router = createRouter({
|
|
|
|
|
history: createWebHistory(),
|
|
|
|
|
routes: [
|
|
|
|
|
{
|
|
|
|
|
path: '/',
|
|
|
|
|
component: () => import('../views/OnboardingWrapper.vue'),
|
|
|
|
|
meta: { public: true },
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
path: '',
|
2026-02-17 15:03:34 +00:00
|
|
|
component: () => import('../views/RootRedirect.vue'),
|
2026-01-24 22:59:20 +00:00
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'login',
|
|
|
|
|
name: 'login',
|
|
|
|
|
component: () => import('../views/Login.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/intro',
|
|
|
|
|
name: 'onboarding-intro',
|
|
|
|
|
component: () => import('../views/OnboardingIntro.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/options',
|
|
|
|
|
name: 'onboarding-options',
|
|
|
|
|
component: () => import('../views/OnboardingOptions.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/path',
|
|
|
|
|
name: 'onboarding-path',
|
|
|
|
|
component: () => import('../views/OnboardingPath.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/did',
|
|
|
|
|
name: 'onboarding-did',
|
|
|
|
|
component: () => import('../views/OnboardingDid.vue'),
|
|
|
|
|
},
|
2026-03-09 07:43:12 +00:00
|
|
|
{
|
|
|
|
|
path: 'onboarding/identity',
|
|
|
|
|
name: 'onboarding-identity',
|
|
|
|
|
component: () => import('../views/OnboardingIdentity.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'onboarding/backup',
|
|
|
|
|
name: 'onboarding-backup',
|
|
|
|
|
component: () => import('../views/OnboardingBackup.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/verify',
|
|
|
|
|
name: 'onboarding-verify',
|
|
|
|
|
component: () => import('../views/OnboardingVerify.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'onboarding/done',
|
|
|
|
|
name: 'onboarding-done',
|
|
|
|
|
component: () => import('../views/OnboardingDone.vue'),
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: '/recovery',
|
|
|
|
|
name: 'recovery',
|
|
|
|
|
component: () => import('../views/KioskRecovery.vue'),
|
|
|
|
|
meta: { public: true },
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: '/kiosk',
|
|
|
|
|
name: 'kiosk',
|
|
|
|
|
component: () => import('../views/Kiosk.vue'),
|
|
|
|
|
meta: { public: true },
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: '/dashboard',
|
|
|
|
|
component: () => import('../views/Dashboard.vue'),
|
|
|
|
|
children: [
|
|
|
|
|
{
|
|
|
|
|
path: '',
|
|
|
|
|
name: 'home',
|
|
|
|
|
component: () => import('../views/Home.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'apps',
|
|
|
|
|
name: 'apps',
|
|
|
|
|
component: () => import('../views/Apps.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'apps/:id',
|
|
|
|
|
name: 'app-details',
|
|
|
|
|
component: () => import('../views/AppDetails.vue'),
|
|
|
|
|
},
|
2026-03-09 07:43:12 +00:00
|
|
|
{
|
|
|
|
|
path: 'apps/lnd/channels',
|
|
|
|
|
name: 'lightning-channels',
|
|
|
|
|
component: () => import('../views/apps/LightningChannels.vue'),
|
|
|
|
|
},
|
feat: add Discover page — cypherpunk app store with sovereignty messaging
- New Discover.vue with hero banner, featured sovereignty stack apps,
principle cards, manifesto footer, and full app grid
- Featured apps (Bitcoin Knots, LND, BTCPay, Vaultwarden) with
expanded privacy/sovereignty descriptions
- Discover is first tab in categories bar on App Store pages
- Smart back navigation: detail pages return to Discover when navigated from there
- Category clicks from Discover navigate to Marketplace with category pre-selected
- Cypherpunk aesthetic: terminal tags, scanline overlays, gradient accents,
animated Bitcoin orange headings
- Global CSS classes: discover-hero, discover-terminal-tag, discover-featured-card,
discover-principle-card, discover-manifesto
- Route added: /dashboard/discover
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:14:12 +00:00
|
|
|
{
|
|
|
|
|
path: 'discover',
|
|
|
|
|
name: 'discover',
|
|
|
|
|
component: () => import('../views/Discover.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'marketplace',
|
|
|
|
|
name: 'marketplace',
|
|
|
|
|
component: () => import('../views/Marketplace.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'marketplace/:id',
|
|
|
|
|
name: 'marketplace-app-detail',
|
|
|
|
|
component: () => import('../views/MarketplaceAppDetails.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'cloud',
|
|
|
|
|
name: 'cloud',
|
|
|
|
|
component: () => import('../views/Cloud.vue'),
|
|
|
|
|
},
|
2026-03-13 02:37:59 +00:00
|
|
|
{
|
2026-03-14 17:12:41 +00:00
|
|
|
path: 'cloud/peers/:peerId?',
|
2026-03-13 02:37:59 +00:00
|
|
|
name: 'peer-files',
|
|
|
|
|
component: () => import('../views/PeerFiles.vue'),
|
2026-03-14 17:12:41 +00:00
|
|
|
props: true,
|
2026-03-13 02:37:59 +00:00
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'cloud/:folderId',
|
|
|
|
|
name: 'cloud-folder',
|
|
|
|
|
component: () => import('../views/CloudFolder.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'server',
|
|
|
|
|
name: 'server',
|
|
|
|
|
component: () => import('../views/Server.vue'),
|
|
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: 'monitoring',
|
|
|
|
|
name: 'monitoring',
|
|
|
|
|
component: () => import('../views/Monitoring.vue'),
|
|
|
|
|
},
|
feat: add Discover page — cypherpunk app store with sovereignty messaging
- New Discover.vue with hero banner, featured sovereignty stack apps,
principle cards, manifesto footer, and full app grid
- Featured apps (Bitcoin Knots, LND, BTCPay, Vaultwarden) with
expanded privacy/sovereignty descriptions
- Discover is first tab in categories bar on App Store pages
- Smart back navigation: detail pages return to Discover when navigated from there
- Category clicks from Discover navigate to Marketplace with category pre-selected
- Cypherpunk aesthetic: terminal tags, scanline overlays, gradient accents,
animated Bitcoin orange headings
- Global CSS classes: discover-hero, discover-terminal-tag, discover-featured-card,
discover-principle-card, discover-manifesto
- Route added: /dashboard/discover
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 15:14:12 +00:00
|
|
|
{
|
|
|
|
|
path: 'fleet',
|
|
|
|
|
name: 'fleet',
|
|
|
|
|
component: () => import('../views/Fleet.vue'),
|
|
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: 'server/federation',
|
|
|
|
|
name: 'federation',
|
|
|
|
|
component: () => import('../views/Federation.vue'),
|
|
|
|
|
},
|
2026-03-17 00:03:08 +00:00
|
|
|
{
|
|
|
|
|
path: 'mesh',
|
|
|
|
|
name: 'mesh',
|
|
|
|
|
component: () => import('../views/Mesh.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'web5',
|
|
|
|
|
name: 'web5',
|
2026-03-21 02:43:28 +00:00
|
|
|
component: () => import('../views/web5/Web5.vue'),
|
2026-01-24 22:59:20 +00:00
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: 'web5/credentials',
|
|
|
|
|
name: 'credentials',
|
|
|
|
|
component: () => import('../views/Credentials.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
{
|
|
|
|
|
path: 'settings',
|
|
|
|
|
name: 'settings',
|
|
|
|
|
component: () => import('../views/Settings.vue'),
|
|
|
|
|
},
|
2026-03-12 00:19:30 +00:00
|
|
|
{
|
|
|
|
|
path: 'settings/update',
|
|
|
|
|
name: 'system-update',
|
|
|
|
|
component: () => import('../views/SystemUpdate.vue'),
|
|
|
|
|
},
|
2026-03-04 07:09:31 +00:00
|
|
|
{
|
|
|
|
|
path: 'goals/:goalId',
|
|
|
|
|
name: 'goal-detail',
|
|
|
|
|
component: () => import('../views/GoalDetail.vue'),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'chat',
|
|
|
|
|
name: 'chat',
|
|
|
|
|
component: () => import('../views/Chat.vue'),
|
|
|
|
|
},
|
2026-03-15 00:40:55 +00:00
|
|
|
{
|
|
|
|
|
path: 'app-session/:appId',
|
|
|
|
|
name: 'app-session',
|
|
|
|
|
component: () => import('../views/AppSession.vue'),
|
|
|
|
|
},
|
2026-02-14 16:44:20 +00:00
|
|
|
// Containers removed: My Apps serves the same purpose. Redirect old links.
|
|
|
|
|
{
|
|
|
|
|
path: 'containers',
|
|
|
|
|
redirect: () => ({ path: '/dashboard/apps' }),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
path: 'containers/:id',
|
|
|
|
|
redirect: (to) => ({ path: `/dashboard/apps/${to.params.id}` }),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
],
|
|
|
|
|
},
|
2026-03-15 04:50:24 +00:00
|
|
|
{
|
|
|
|
|
path: '/:pathMatch(.*)*',
|
|
|
|
|
name: 'not-found',
|
|
|
|
|
component: () => import('../views/NotFound.vue'),
|
|
|
|
|
},
|
2026-01-24 22:59:20 +00:00
|
|
|
],
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-02 08:34:13 +00:00
|
|
|
// Session check with timeout - avoids endless spinner on mobile/slow networks
|
|
|
|
|
const SESSION_CHECK_TIMEOUT_MS = 8000
|
|
|
|
|
|
|
|
|
|
async function checkSessionWithTimeout(store: ReturnType<typeof useAppStore>): Promise<boolean> {
|
|
|
|
|
try {
|
|
|
|
|
return await Promise.race([
|
|
|
|
|
store.checkSession(),
|
|
|
|
|
new Promise<boolean>((resolve) =>
|
|
|
|
|
setTimeout(() => resolve(false), SESSION_CHECK_TIMEOUT_MS)
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
/**
|
|
|
|
|
* Navigation Guard
|
|
|
|
|
* Handles authentication and onboarding flow routing
|
|
|
|
|
*/
|
security+feat: v1.3.0 — pentest remediation, container reliability, UI overhaul
Security (33 pentest findings addressed):
- CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed
- HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted
- HIGH: tar slip prevention, S3 SSRF validation, backup ID validation
- MEDIUM: remember-me random secret, TOTP session rotation, password re-auth
- LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation
Container reliability:
- Memory limits on all 37 containers (OOM prevention)
- Exited vs stopped state distinction with health-aware status badges
- Crash recovery coordination (no more restart cascade)
- User-stopped tracking survives reboots
- Tiered boot recovery (databases → core → services → apps)
UI:
- Wallet TransactionsModal, health-aware app status badges
- Restart button on containers, exited/crashed red state
- Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch
- Apps sticky header removed, dev faucet, mutable mock wallet
Infrastructure:
- LND REST port 8080 exposed over Tor (LND Connect fix)
- Nginx cookie_session fix, deploy script Tor config updated
- Dev environment: podman auto-start, boot mode simulation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 12:44:31 +00:00
|
|
|
export function isLocalRedirect(path: unknown): path is string {
|
2026-03-18 00:55:00 +00:00
|
|
|
if (typeof path !== 'string') return false
|
|
|
|
|
try {
|
|
|
|
|
if (path.startsWith('//') || path.includes('://')) return false
|
|
|
|
|
const url = new URL(path, window.location.origin)
|
|
|
|
|
return url.origin === window.location.origin
|
|
|
|
|
} catch {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
router.beforeEach(async (to, _from, next) => {
|
|
|
|
|
const store = useAppStore()
|
|
|
|
|
const isPublic = to.meta.public
|
|
|
|
|
|
|
|
|
|
// Allow all public routes (login, onboarding) without auth check
|
|
|
|
|
if (isPublic) {
|
2026-03-02 08:34:13 +00:00
|
|
|
// If authenticated and visiting /login: show login immediately, validate in background.
|
|
|
|
|
// This prevents endless spinner on mobile when checkSession hangs (slow/unreachable network).
|
2026-01-24 22:59:20 +00:00
|
|
|
if (to.path === '/login' && store.isAuthenticated) {
|
2026-03-16 12:58:35 +00:00
|
|
|
// Redirect back to intended page (from ?redirect= query) or default to home
|
2026-03-18 00:55:00 +00:00
|
|
|
const rawRedirect = to.query.redirect
|
|
|
|
|
const redirectTo = isLocalRedirect(rawRedirect) ? rawRedirect : '/dashboard'
|
2026-03-01 17:53:18 +00:00
|
|
|
if (store.needsSessionValidation()) {
|
|
|
|
|
next()
|
2026-03-02 08:34:13 +00:00
|
|
|
checkSessionWithTimeout(store).then((valid) => {
|
|
|
|
|
if (valid) {
|
2026-03-16 12:58:35 +00:00
|
|
|
router.replace(redirectTo).catch(() => {})
|
2026-03-02 08:34:13 +00:00
|
|
|
}
|
|
|
|
|
})
|
2026-03-01 17:53:18 +00:00
|
|
|
return
|
|
|
|
|
}
|
2026-03-16 12:58:35 +00:00
|
|
|
next(redirectTo)
|
2026-01-24 22:59:20 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
next()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-05 07:22:20 +00:00
|
|
|
// Protected routes: validate session if stale auth from localStorage
|
2026-03-01 17:53:18 +00:00
|
|
|
if (store.needsSessionValidation()) {
|
2026-03-05 07:22:20 +00:00
|
|
|
// localStorage says we're authed — proceed immediately, revalidate in background.
|
|
|
|
|
// No timeout wrapper here: a slow server shouldn't bounce the user to login.
|
2026-03-01 17:53:18 +00:00
|
|
|
next()
|
2026-03-05 07:22:20 +00:00
|
|
|
store.checkSession().then((valid) => {
|
|
|
|
|
if (!valid) {
|
2026-03-16 12:58:35 +00:00
|
|
|
router.replace({ path: '/login', query: { redirect: to.fullPath } }).catch(() => {})
|
2026-03-05 07:22:20 +00:00
|
|
|
}
|
|
|
|
|
})
|
2026-03-01 17:53:18 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-02 08:34:13 +00:00
|
|
|
// Not authenticated at all (with timeout to avoid endless spinner on mobile)
|
2026-01-24 22:59:20 +00:00
|
|
|
if (!store.isAuthenticated) {
|
2026-03-02 08:34:13 +00:00
|
|
|
const hasSession = await checkSessionWithTimeout(store)
|
2026-01-24 22:59:20 +00:00
|
|
|
if (hasSession) {
|
|
|
|
|
next()
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-28 02:51:02 +00:00
|
|
|
// Check if this is a fresh install that needs onboarding
|
|
|
|
|
try {
|
|
|
|
|
const { isOnboardingComplete } = await import('@/composables/useOnboarding')
|
|
|
|
|
const setupDone = await isOnboardingComplete()
|
|
|
|
|
if (!setupDone) {
|
|
|
|
|
next('/onboarding/intro')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// If we can't check, assume fresh install and show onboarding
|
|
|
|
|
next('/onboarding/intro')
|
|
|
|
|
return
|
|
|
|
|
}
|
2026-03-16 12:58:35 +00:00
|
|
|
next({ path: '/login', query: { redirect: to.fullPath } })
|
2026-01-24 22:59:20 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-01 17:53:18 +00:00
|
|
|
// Validated and authenticated - ensure WebSocket is connected
|
2026-02-01 13:24:03 +00:00
|
|
|
if (!store.isConnected && !store.isReconnecting) {
|
|
|
|
|
store.connectWebSocket().catch((err) => {
|
2026-03-12 00:19:30 +00:00
|
|
|
if (import.meta.env.DEV) console.warn('[Router] WebSocket connection failed:', err)
|
2026-02-01 13:24:03 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
next()
|
|
|
|
|
})
|
|
|
|
|
|
2026-03-12 00:19:30 +00:00
|
|
|
// Stop all login/splash audio when entering the dashboard
|
|
|
|
|
router.afterEach((to, from) => {
|
|
|
|
|
if (to.path.startsWith('/dashboard') && !from.path.startsWith('/dashboard')) {
|
|
|
|
|
stopAllAudio()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-17 19:19:54 +00:00
|
|
|
// Focus Home nav item for gamepad when landing on dashboard home (e.g. after login)
|
|
|
|
|
router.afterEach((to) => {
|
|
|
|
|
if (to.path === '/dashboard' || to.path === '/dashboard/') {
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
const homeLink = document.querySelector<HTMLAnchorElement>(
|
|
|
|
|
'[data-controller-zone="sidebar"] a[href="/dashboard"], [data-controller-zone="sidebar"] a[href="/dashboard/"]'
|
|
|
|
|
)
|
|
|
|
|
if (homeLink) homeLink.focus()
|
|
|
|
|
}, 150)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2026-01-24 22:59:20 +00:00
|
|
|
export default router
|
|
|
|
|
|