diff --git a/.env.example b/.env.example index 1643766..c92c023 100644 --- a/.env.example +++ b/.env.example @@ -42,63 +42,3 @@ VITE_MARKET_NADDR=naddr1qqjxgdp4vv6rydej943n2dny956rwwf4943xzwfc95ekyd3evenrsvrr # VITE_LIGHTNING_ENABLED=true # OBSOLETE: Not used in codebase - config.market.defaultCurrency is never consumed # VITE_MARKET_DEFAULT_CURRENCY=sat - -# ─────────────────────────────────────────────────────────────────────── -# Hub → standalone navigation URLs -# -# Each chakra tile in the hub builds an from these env vars and -# (when authenticated) appends ?token= so the destination -# auto-logs in via acceptTokenFromUrl(). -# -# Trailing slash matters under path-mode deployment: -# ✓ https://demo.example.com/market/ asset URLs resolve correctly -# ✗ https://demo.example.com/market relies on nginx 301 to add the -# slash; brittle, extra round trip. -# -# In LOCAL DEV with `npm run dev:all` use the per-app dev ports (defined -# in the vite configs): -# VITE_HUB_ACTIVITIES_URL=http://localhost:5181 -# VITE_HUB_CASTLE_URL=http://localhost:5180 -# VITE_HUB_WALLET_URL=http://localhost:5182 -# VITE_HUB_CHAT_URL=http://localhost:5183 -# VITE_HUB_FORUM_URL=http://localhost:5184 -# VITE_HUB_MARKET_URL=http://localhost:5185 -# VITE_HUB_TASKS_URL=http://localhost:5186 -# -# In PATH-MODE production (recommended for demo) — note the trailing slash: -# VITE_HUB_ACTIVITIES_URL=https://demo.example.com/activities/ -# VITE_HUB_CASTLE_URL=https://demo.example.com/castle/ -# VITE_HUB_WALLET_URL=https://demo.example.com/wallet/ -# VITE_HUB_CHAT_URL=https://demo.example.com/chat/ -# VITE_HUB_FORUM_URL=https://demo.example.com/forum/ -# VITE_HUB_MARKET_URL=https://demo.example.com/market/ -# VITE_HUB_TASKS_URL=https://demo.example.com/tasks/ -# -# In SUBDOMAIN-MODE production: -# VITE_HUB_ACTIVITIES_URL=https://sortir.example.com -# VITE_HUB_CASTLE_URL=https://castle.example.com -# ...etc -# ─────────────────────────────────────────────────────────────────────── -VITE_HUB_ACTIVITIES_URL= -VITE_HUB_CASTLE_URL= -VITE_HUB_WALLET_URL= -VITE_HUB_CHAT_URL= -VITE_HUB_FORUM_URL= -VITE_HUB_MARKET_URL= -VITE_HUB_TASKS_URL= - -# ─────────────────────────────────────────────────────────────────────── -# VITE_BASE_PATH — build-time only, NOT per .env -# -# Each standalone vite config (vite..config.ts) reads VITE_BASE_PATH -# at build time. For path-mode deployment, set it as a shell variable when -# you build, NOT in this .env file (which is read at runtime by the -# bundle): -# -# VITE_BASE_PATH=/market/ npm run build:market -# VITE_BASE_PATH=/wallet/ npm run build:wallet -# ... -# -# The default '/' (no override) is what you want for subdomain-mode and -# for `npm run dev:all`. -# ─────────────────────────────────────────────────────────────────────── diff --git a/activities.html b/activities.html index b555a4d..d227e52 100644 --- a/activities.html +++ b/activities.html @@ -3,7 +3,6 @@ - diff --git a/castle.html b/castle.html index cee9c6f..fe3f567 100644 --- a/castle.html +++ b/castle.html @@ -3,7 +3,6 @@ - diff --git a/chat.html b/chat.html deleted file mode 100644 index fb83dee..0000000 --- a/chat.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - Chat — Encrypted - - - - -
- - - diff --git a/forum.html b/forum.html deleted file mode 100644 index 8646936..0000000 --- a/forum.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - Forum — Discussions - - - - -
- - - diff --git a/index.html b/index.html index 76e7922..c1f84b2 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,6 @@ - diff --git a/market.html b/market.html deleted file mode 100644 index 3fc32d5..0000000 --- a/market.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - Market — Nostr - - - - -
- - - diff --git a/nginx.conf.example b/nginx.conf.example index 662b4c9..e22b16e 100644 --- a/nginx.conf.example +++ b/nginx.conf.example @@ -1,169 +1,45 @@ # Main context -worker_processes auto; +worker_processes auto; # Automatically determine worker processes based on CPU cores events { - worker_connections 1024; + worker_connections 1024; # Maximum connections per worker } http { default_type application/octet-stream; + # Trust the custom Docker network subnet set_real_ip_from 0.0.0.0; real_ip_header X-Forwarded-For; real_ip_recursive on; - # ─────────────────────────────────────────────────────────────────────── - # PATH-MODE deployment (recommended) - # - # demo../ — minimal AIO chakra hub - # demo../activities/ — Sortir / activities standalone - # demo../market/ — marketplace standalone - # demo../wallet/ — wallet standalone - # demo../chat/ — chat standalone - # demo../forum/ — forum standalone - # demo../tasks/ — tasks standalone - # demo../castle/ — castle (accounting) standalone - # - # Each standalone is built with VITE_BASE_PATH=// so its asset URLs - # are prefixed correctly. The hub's chakra tiles point at the canonical - # trailing-slash path (VITE_HUB__URL=https://demo.//). - # - # Per-app no-trailing-slash → with-slash 301 redirects exist for users - # who hand-type the URL or follow a stripped-slash link. - # - # All static assets (JS / CSS / images / SVGs) are MIME-typed and image - # types get a 6-month cache-control. - # ─────────────────────────────────────────────────────────────────────── server { listen 8080; - server_name demo..; + server_name .; - # Hub at the root - root /var/www/aio/dist; + root /app; index index.html; - location = / { try_files $uri /index.html; } + location / { - # Default: serve from hub bundle if no // prefix matched. try_files $uri $uri/ /index.html; } - # ── Activities (Sortir) ────────────────────────────────────────── - location = /activities { return 301 /activities/$is_args$args; } - location /activities/ { - alias /var/www/aio/dist-activities/; - try_files $uri $uri/ /activities.html; - } - - # ── Market ─────────────────────────────────────────────────────── - location = /market { return 301 /market/$is_args$args; } - location /market/ { - alias /var/www/aio/dist-market/; - try_files $uri $uri/ /market.html; - } - - # ── Wallet ─────────────────────────────────────────────────────── - location = /wallet { return 301 /wallet/$is_args$args; } - location /wallet/ { - alias /var/www/aio/dist-wallet/; - try_files $uri $uri/ /wallet.html; - } - - # ── Chat ───────────────────────────────────────────────────────── - location = /chat { return 301 /chat/$is_args$args; } - location /chat/ { - alias /var/www/aio/dist-chat/; - try_files $uri $uri/ /chat.html; - } - - # ── Forum ──────────────────────────────────────────────────────── - location = /forum { return 301 /forum/$is_args$args; } - location /forum/ { - alias /var/www/aio/dist-forum/; - try_files $uri $uri/ /forum.html; - } - - # ── Tasks ──────────────────────────────────────────────────────── - location = /tasks { return 301 /tasks/$is_args$args; } - location /tasks/ { - alias /var/www/aio/dist-tasks/; - try_files $uri $uri/ /tasks.html; - } - - # ── Castle (accounting) ────────────────────────────────────────── - location = /castle { return 301 /castle/$is_args$args; } - location /castle/ { - alias /var/www/aio/dist-castle/; - try_files $uri $uri/ /castle.html; - } - - # ── Static asset MIME / cache (applies to all bundles) ─────────── - location ~* \.js$ { types { application/javascript js; } default_type application/javascript; } - location ~* \.css$ { types { text/css css; } default_type text/css; } - location ~* \.(png|jpe?g|webp|ico|svg)$ { expires 6M; access_log off; } + location ~* \.js$ { + types { application/javascript js; } + default_type application/javascript; } - # ─────────────────────────────────────────────────────────────────────── - # Optional subdomain shortcuts → canonical path - # - # If you want pretty subdomain URLs that funnel into the path-mode - # canonical, add 301 redirects per app. Example: - # - # events.demo.. → demo../activities/ - # market.demo.. → demo../market/ - # ─────────────────────────────────────────────────────────────────────── - server { - listen 8080; - server_name events.demo..; - return 301 https://demo../activities/$request_uri; - } - server { - listen 8080; - server_name market.demo..; - return 301 https://demo../market/$request_uri; - } - server { - listen 8080; - server_name wallet.demo..; - return 301 https://demo../wallet/$request_uri; - } - server { - listen 8080; - server_name chat.demo..; - return 301 https://demo../chat/$request_uri; - } - server { - listen 8080; - server_name forum.demo..; - return 301 https://demo../forum/$request_uri; - } - server { - listen 8080; - server_name tasks.demo..; - return 301 https://demo../tasks/$request_uri; - } - server { - listen 8080; - server_name castle.demo..; - return 301 https://demo../castle/$request_uri; + # Serve CSS files with the correct MIME type + location ~* \.css$ { + types { text/css css; } + default_type text/css; } - # ─────────────────────────────────────────────────────────────────────── - # SUBDOMAIN-MODE deployment (alternative — pure subdomains, no /path/) - # - # If you'd rather give each standalone its own subdomain and skip the - # path-mode entirely: - # - # server { server_name app.; root /var/www/aio/dist; ... } - # server { server_name market.; root /var/www/aio/dist-market; ... } - # server { server_name sortir.; root /var/www/aio/dist-activities; ... } - # server { server_name wallet.; root /var/www/aio/dist-wallet; ... } - # server { server_name chat.; root /var/www/aio/dist-chat; ... } - # server { server_name forum.; root /var/www/aio/dist-forum; ... } - # server { server_name tasks.; root /var/www/aio/dist-tasks; ... } - # server { server_name castle.; root /var/www/aio/dist-castle; ... } - # - # Each block uses `location / { try_files $uri $uri/ /.html; }`. - # In subdomain mode, build each standalone WITHOUT VITE_BASE_PATH (the - # default `/` is correct), and set VITE_HUB__URL to the subdomain - # in the hub's env (e.g. VITE_HUB_MARKET_URL=https://market.). - # ─────────────────────────────────────────────────────────────────────── + # Serve image files + location ~* \.(png|jpe?g|webp|ico)$ { + expires 6M; # Optional: Cache static assets for 6 months + access_log off; + } + + } } + diff --git a/package.json b/package.json index b1b19cc..da537c9 100644 --- a/package.json +++ b/package.json @@ -15,23 +15,6 @@ "dev:castle": "vite --host --config vite.castle.config.ts", "build:castle": "vue-tsc -b && vite build --config vite.castle.config.ts", "preview:castle": "vite preview --host --config vite.castle.config.ts", - "dev:wallet": "vite --host --config vite.wallet.config.ts", - "build:wallet": "vue-tsc -b && vite build --config vite.wallet.config.ts", - "preview:wallet": "vite preview --host --config vite.wallet.config.ts", - "dev:chat": "vite --host --config vite.chat.config.ts", - "build:chat": "vue-tsc -b && vite build --config vite.chat.config.ts", - "preview:chat": "vite preview --host --config vite.chat.config.ts", - "dev:market": "vite --host --config vite.market.config.ts", - "build:market": "vue-tsc -b && vite build --config vite.market.config.ts", - "preview:market": "vite preview --host --config vite.market.config.ts", - "dev:tasks": "vite --host --config vite.tasks.config.ts", - "build:tasks": "vue-tsc -b && vite build --config vite.tasks.config.ts", - "preview:tasks": "vite preview --host --config vite.tasks.config.ts", - "dev:forum": "vite --host --config vite.forum.config.ts", - "build:forum": "vue-tsc -b && vite build --config vite.forum.config.ts", - "preview:forum": "vite preview --host --config vite.forum.config.ts", - "dev:all": "concurrently -n hub,castle,sortir,wallet,chat,forum,market,tasks -c blue,magenta,cyan,yellow,green,blue,red,gray \"npm:dev\" \"npm:dev:castle\" \"npm:dev:activities\" \"npm:dev:wallet\" \"npm:dev:chat\" \"npm:dev:forum\" \"npm:dev:market\" \"npm:dev:tasks\"", - "build:demo": "npm run build && VITE_BASE_PATH=/sortir/ npm run build:activities && VITE_BASE_PATH=/castle/ npm run build:castle && VITE_BASE_PATH=/wallet/ npm run build:wallet && VITE_BASE_PATH=/chat/ npm run build:chat && VITE_BASE_PATH=/forum/ npm run build:forum && VITE_BASE_PATH=/market/ npm run build:market && VITE_BASE_PATH=/tasks/ npm run build:tasks", "electron:dev": "concurrently \"vite --host\" \"electron-forge start\"", "electron:build": "vue-tsc -b && vite build && electron-builder", "electron:package": "electron-builder", diff --git a/public/chakras/ajna.svg b/public/chakras/ajna.svg deleted file mode 100644 index 212d4e4..0000000 --- a/public/chakras/ajna.svg +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - diff --git a/public/chakras/anahata.svg b/public/chakras/anahata.svg deleted file mode 100644 index a2cc52b..0000000 --- a/public/chakras/anahata.svg +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/chakras/manipura.svg b/public/chakras/manipura.svg deleted file mode 100644 index 0c87aa8..0000000 --- a/public/chakras/manipura.svg +++ /dev/null @@ -1,92 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/public/chakras/muladhara.svg b/public/chakras/muladhara.svg deleted file mode 100644 index cd21ee8..0000000 --- a/public/chakras/muladhara.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/public/chakras/sahasrara.svg b/public/chakras/sahasrara.svg deleted file mode 100644 index f8f6323..0000000 --- a/public/chakras/sahasrara.svg +++ /dev/null @@ -1,132 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/public/chakras/swadhisthana.svg b/public/chakras/swadhisthana.svg deleted file mode 100644 index 9123a7d..0000000 --- a/public/chakras/swadhisthana.svg +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/public/chakras/vishuddha.svg b/public/chakras/vishuddha.svg deleted file mode 100644 index 7b71e72..0000000 --- a/public/chakras/vishuddha.svg +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/App.vue b/src/App.vue index 8c7f8ba..559d0bf 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,47 +1,77 @@ diff --git a/src/accounting-app/app.ts b/src/accounting-app/app.ts index eb20adf..bdf83e4 100644 --- a/src/accounting-app/app.ts +++ b/src/accounting-app/app.ts @@ -14,8 +14,26 @@ import App from './App.vue' import '@/assets/index.css' import { i18n, changeLocale, type AvailableLocale } from '@/i18n' -import { installStrictAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers' -import { acceptTokenFromUrl } from '@/lib/url-token' + +/** + * Accept an auth token from a URL parameter (e.g. ?token=xxx). + * This allows the main app to link users directly into Castle + * without requiring a separate login. The token is stored in + * localStorage and the parameter is stripped from the URL. + */ +function acceptTokenFromUrl() { + const params = new URLSearchParams(window.location.search) + const token = params.get('token') + if (token) { + localStorage.setItem('lnbits_access_token', token) + // Also persist user data key so auth service picks it up + params.delete('token') + const clean = params.toString() + const newUrl = window.location.pathname + (clean ? `?${clean}` : '') + window.location.hash + window.history.replaceState({}, '', newUrl) + console.log('[Castle] Auth token accepted from URL') + } +} /** * Initialize the standalone Castle accounting app @@ -24,7 +42,7 @@ export async function createAppInstance() { console.log('Starting Castle — Accounting App...') // Accept token from URL before anything else (cross-subdomain auth relay) - acceptTokenFromUrl('Castle') + acceptTokenFromUrl() const app = createApp(App) @@ -71,13 +89,9 @@ 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) @@ -117,11 +131,22 @@ export async function createAppInstance() { await Promise.all(moduleRegistrations) await pluginManager.installAll() - // Dynamic import: useAuthService depends on services registered by - // pluginManager.installAll() (LNbits API). + // Initialize auth const { auth } = await import('@/composables/useAuthService') await auth.initialize() - markAuthReady(auth) + + // 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() + } + }) // Global error handling app.config.errorHandler = (err, _vm, info) => { diff --git a/src/accounting-app/main.ts b/src/accounting-app/main.ts index ed477b2..22efcbc 100644 --- a/src/accounting-app/main.ts +++ b/src/accounting-app/main.ts @@ -1,10 +1,7 @@ import { startApp } from './app' import { registerSW } from 'virtual:pwa-register' -import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup' import 'vue-sonner/style.css' -cleanupStaleDevServiceWorkers() - // PWA service worker with periodic updates const intervalMS = 60 * 60 * 1000 // 1 hour registerSW({ diff --git a/src/activities-app/app.ts b/src/activities-app/app.ts index 581a6e2..918ed38 100644 --- a/src/activities-app/app.ts +++ b/src/activities-app/app.ts @@ -13,8 +13,24 @@ import App from './App.vue' import '@/assets/index.css' import { i18n, changeLocale, type AvailableLocale } from '@/i18n' -import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers' -import { acceptTokenFromUrl } from '@/lib/url-token' + +/** + * Accept an auth token from a URL parameter (e.g. ?token=xxx). + * Allows the main app to link users directly into this standalone + * app without requiring a separate login. + */ +function acceptTokenFromUrl() { + const params = new URLSearchParams(window.location.search) + const token = params.get('token') + if (token) { + localStorage.setItem('lnbits_access_token', token) + params.delete('token') + const clean = params.toString() + const newUrl = window.location.pathname + (clean ? `?${clean}` : '') + window.location.hash + window.history.replaceState({}, '', newUrl) + console.log('[Sortir] Auth token accepted from URL') + } +} /** * Initialize the standalone activities app @@ -23,7 +39,7 @@ export async function createAppInstance() { console.log('🚀 Starting Sortir — Activities App...') // Accept token from URL before anything else (cross-subdomain auth relay) - acceptTokenFromUrl('Sortir') + acceptTokenFromUrl() const app = createApp(App) @@ -57,12 +73,9 @@ export async function createAppInstance() { component: () => import('./views/SettingsPage.vue'), meta: { requiresAuth: false } }, - catchAllRoute, ] }) - installLenientAuthGuard(router) - const pinia = createPinia() app.use(router) @@ -96,11 +109,22 @@ export async function createAppInstance() { await Promise.all(moduleRegistrations) await pluginManager.installAll() - // Dynamic import: useAuthService depends on services registered by - // pluginManager.installAll() (LNbits API). + // Initialize auth const { auth } = await import('@/composables/useAuthService') await auth.initialize() - markAuthReady(auth) + + // 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() + } + }) // Global error handling app.config.errorHandler = (err, _vm, info) => { diff --git a/src/activities-app/main.ts b/src/activities-app/main.ts index e3bb49d..c9c8429 100644 --- a/src/activities-app/main.ts +++ b/src/activities-app/main.ts @@ -1,10 +1,7 @@ import { startApp } from './app' import { registerSW } from 'virtual:pwa-register' -import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup' import 'vue-sonner/style.css' -cleanupStaleDevServiceWorkers() - // PWA service worker with periodic updates const intervalMS = 60 * 60 * 1000 // 1 hour registerSW({ diff --git a/src/app.config.ts b/src/app.config.ts index dce6464..da83333 100644 --- a/src/app.config.ts +++ b/src/app.config.ts @@ -1,11 +1,12 @@ import type { AppConfig } from './core/types' -/** - * Minimal AIO hub configuration. - * The all-in-one app at app.${domain} ships only the base module — - * each feature module (wallet, chat, market, tasks, forum, activities, - * castle) is now its own standalone PWA at its own subdomain. - */ +function parseMapCenter(envValue: string | undefined, fallback: { lat: number; lng: number }) { + if (!envValue) return fallback + const [lat, lng] = envValue.split(',').map(Number) + if (isNaN(lat) || isNaN(lng)) return fallback + return { lat, lng } +} + export const appConfig: AppConfig = { modules: { base: { @@ -17,7 +18,7 @@ export const appConfig: AppConfig = { relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '["wss://relay.damus.io", "wss://nos.lol"]') }, auth: { - sessionTimeout: 24 * 60 * 60 * 1000, + sessionTimeout: 24 * 60 * 60 * 1000, // 24 hours }, pwa: { autoPrompt: true @@ -28,9 +29,115 @@ export const appConfig: AppConfig = { acceptedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'] } } + }, + 'nostr-feed': { + name: 'nostr-feed', + enabled: false, // Disabled - replaced by links module + lazy: false, + config: { + refreshInterval: 30000, + maxPosts: 100, + adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]'), + feedTypes: ['announcements', 'general'] + } + }, + links: { + name: 'links', + enabled: true, + lazy: false, + config: { + maxSubmissions: 50, + corsProxyUrl: import.meta.env.VITE_CORS_PROXY_URL || '', + adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]') + } + }, + tasks: { + name: 'tasks', + enabled: true, + lazy: false, + config: { + maxTasks: 200, + adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]') + } + }, + market: { + name: 'market', + enabled: true, + lazy: false, + config: { + defaultCurrency: 'sats', + paymentTimeout: 300000, // 5 minutes + maxOrderHistory: 50, + apiConfig: { + baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000' + } + } + }, + chat: { + name: 'chat', + enabled: true, + lazy: false, // Load on startup to register routes + config: { + maxMessages: 500, + autoScroll: true, + showTimestamps: true, + notifications: { + enabled: true, + soundEnabled: false, + wildcardSupport: true + } + } + }, + activities: { + name: 'activities', + enabled: true, + lazy: false, + config: { + apiConfig: { + baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000', + apiKey: import.meta.env.VITE_API_KEY || '' + }, + defaultMapCenter: parseMapCenter(import.meta.env.VITE_DEFAULT_MAP_CENTER, { lat: 46.6034, lng: 1.8883 }), + maxTicketsPerUser: 10, + enableMap: true, + enablePrivateEvents: false + } + }, + wallet: { + name: 'wallet', + enabled: true, + lazy: false, + config: { + defaultReceiveAmount: 1000, // 1000 sats + maxReceiveAmount: 1000000, // 1M sats + apiConfig: { + baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000' + }, + websocket: { + enabled: import.meta.env.VITE_WEBSOCKET_ENABLED !== 'false', // Can be disabled via env var + reconnectDelay: 2000, // 2 seconds (increased from 1s to reduce server load) + maxReconnectAttempts: 3, // Reduced from 5 to avoid overwhelming server + fallbackToPolling: true, // Enable polling fallback when WebSocket fails + pollingInterval: 10000 // 10 seconds for polling updates + } + } + }, + expenses: { + name: 'expenses', + enabled: true, + lazy: false, + config: { + apiConfig: { + baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000', + timeout: 30000 // 30 seconds for API requests + }, + defaultCurrency: 'sats', + maxExpenseAmount: 1000000, // 1M sats + requireDescription: true + } } }, - + features: { pwa: true, pushNotifications: true, @@ -39,4 +146,4 @@ export const appConfig: AppConfig = { } } -export default appConfig +export default appConfig \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index 73fc5b2..0f45340 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,44 +1,67 @@ import { createApp } from 'vue' import { createRouter, createWebHistory } from 'vue-router' import { createPinia } from 'pinia' +// Core plugin system import { pluginManager } from './core/plugin-manager' import { eventBus } from './core/event-bus' import { container } from './core/di-container' +// App configuration import appConfig from './app.config' +// Base modules import baseModule from './modules/base' +import nostrFeedModule from './modules/nostr-feed' +import chatModule from './modules/chat' +import activitiesModule from './modules/activities' +import marketModule from './modules/market' +import walletModule from './modules/wallet' +import expensesModule from './modules/expenses' +import linksModule from './modules/links' +import tasksModule from './modules/tasks' +// Root component import App from './App.vue' +// Styles import './assets/index.css' + +// Use existing i18n setup import { i18n } from './i18n' -import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers' /** - * Initialize and start the minimal AIO hub. - * - * The all-in-one app at app.${domain} now ships only the base module - * plus a chakra icon hub linking out to the standalone module apps - * (wallet, chat, market, tasks, forum, activities, castle). + * Initialize and start the modular application */ export async function createAppInstance() { - console.log('🚀 Starting AIO hub...') + console.log('🚀 Starting modular application...') + // Create Vue app const app = createApp(App) + // Collect all module routes automatically to avoid duplication const moduleRoutes = [ + // Extract routes from modules directly ...baseModule.routes || [], + ...nostrFeedModule.routes || [], + ...chatModule.routes || [], + ...activitiesModule.routes || [], + ...marketModule.routes || [], + ...walletModule.routes || [], + ...expensesModule.routes || [], + ...linksModule.routes || [], + ...tasksModule.routes || [] ].filter(Boolean) + // Create router with all routes available immediately const router = createRouter({ history: createWebHistory(), routes: [ + // Default routes { path: '/', - name: 'hub', - component: () => import('./pages/Hub.vue'), - meta: { requiresAuth: false } + name: 'home', + component: () => import('./pages/Home.vue'), + meta: { requiresAuth: true } }, { path: '/login', @@ -48,72 +71,175 @@ export async function createAppInstance() { : () => import('./pages/Login.vue'), meta: { requiresAuth: false } }, - ...moduleRoutes, - catchAllRoute, + // Pre-register module routes + ...moduleRoutes ] }) - // Register guards immediately (Vue Router docs: before app.use(router)). - // Guards await auth readiness internally — see router-helpers.ts. - installLenientAuthGuard(router) + // Use existing i18n setup + // Create Pinia store const pinia = createPinia() + // Install core plugins app.use(router) app.use(pinia) app.use(i18n) + // Initialize plugin manager pluginManager.init(app, router) + // Register modules based on configuration const moduleRegistrations = [] + // Register base module first (required) if (appConfig.modules.base.enabled) { moduleRegistrations.push( pluginManager.register(baseModule, appConfig.modules.base) ) } + // Register nostr-feed module + if (appConfig.modules['nostr-feed'].enabled) { + moduleRegistrations.push( + pluginManager.register(nostrFeedModule, appConfig.modules['nostr-feed']) + ) + } + + // Register chat module + if (appConfig.modules.chat.enabled) { + moduleRegistrations.push( + pluginManager.register(chatModule, appConfig.modules.chat) + ) + } + + // Register activities module (events + ticketing) + if (appConfig.modules.activities?.enabled) { + moduleRegistrations.push( + pluginManager.register(activitiesModule, appConfig.modules.activities) + ) + } + + // Register market module + if (appConfig.modules.market.enabled) { + moduleRegistrations.push( + pluginManager.register(marketModule, appConfig.modules.market) + ) + } + + // Register wallet module + if (appConfig.modules.wallet?.enabled) { + moduleRegistrations.push( + pluginManager.register(walletModule, appConfig.modules.wallet) + ) + } + + // Register expenses module + if (appConfig.modules.expenses?.enabled) { + moduleRegistrations.push( + pluginManager.register(expensesModule, appConfig.modules.expenses) + ) + } + + // Register links module + if (appConfig.modules.links?.enabled) { + moduleRegistrations.push( + pluginManager.register(linksModule, appConfig.modules.links) + ) + } + + // Register tasks module + if (appConfig.modules.tasks?.enabled) { + moduleRegistrations.push( + pluginManager.register(tasksModule, appConfig.modules.tasks) + ) + } + + // Wait for all modules to register await Promise.all(moduleRegistrations) + + // Install all enabled modules 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. + // Initialize auth before setting up router guards const { auth } = await import('@/composables/useAuthService') await auth.initialize() - markAuthReady(auth) console.log('Auth initialized, isAuthenticated:', auth.isAuthenticated.value) + // Set up auth guard + router.beforeEach(async (to, _from, next) => { + // Default to requiring auth unless explicitly set to false + const requiresAuth = to.meta.requiresAuth !== false + + if (requiresAuth && !auth.isAuthenticated.value) { + console.log(`Auth guard: User not authenticated, redirecting from ${to.path} to login`) + next('/login') + } else if (to.path === '/login' && auth.isAuthenticated.value) { + console.log('Auth guard: User already authenticated, redirecting to home') + next('/') + } else { + console.log(`Auth guard: Allowing navigation to ${to.path} (requiresAuth: ${requiresAuth}, authenticated: ${auth.isAuthenticated.value})`) + next() + } + }) + + // Check initial route and redirect if needed + if (!auth.isAuthenticated.value) { + const currentRoute = router.currentRoute.value + const requiresAuth = currentRoute.meta.requiresAuth !== false + if (requiresAuth) { + console.log('Initial route requires auth but user not authenticated, redirecting to login') + await router.push('/login') + } + } + + // Global error handling app.config.errorHandler = (err, _vm, info) => { console.error('Global error:', err, info) eventBus.emit('app:error', { error: err, info }, 'app') } + // Development helpers if (appConfig.features.developmentMode) { + // Expose debugging helpers globally ;(window as any).__pluginManager = pluginManager ;(window as any).__eventBus = eventBus ;(window as any).__container = container + + console.log('🔧 Development mode enabled') + console.log('Available globals: __pluginManager, __eventBus, __container') } - console.log('✅ AIO hub initialized') + console.log('✅ Application initialized successfully') + return { app, router } } +/** + * Start the application + */ export async function startApp() { try { const { app } = await createAppInstance() + + // Mount the app app.mount('#app') - console.log('🎉 AIO hub started!') + + console.log('🎉 Application started!') + + // Emit app started event eventBus.emit('app:started', {}, 'app') + } catch (error) { - console.error('💥 Failed to start AIO hub:', error) + console.error('💥 Failed to start application:', error) + + // Show error to user document.getElementById('app')!.innerHTML = `
-

AIO hub failed to start

+

Application Failed to Start

${error instanceof Error ? error.message : 'Unknown error'}

Please refresh the page or contact support.

` } -} +} \ No newline at end of file diff --git a/src/chat-app/App.vue b/src/chat-app/App.vue deleted file mode 100644 index 8c7f8ba..0000000 --- a/src/chat-app/App.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/src/chat-app/app.config.ts b/src/chat-app/app.config.ts deleted file mode 100644 index 8fa20b8..0000000 --- a/src/chat-app/app.config.ts +++ /dev/null @@ -1,55 +0,0 @@ -import type { AppConfig } from '@/core/types' - -/** - * Standalone Chat app configuration. - * Only enables base + chat modules. - */ -export const appConfig: AppConfig = { - modules: { - base: { - name: 'base', - enabled: true, - lazy: false, - config: { - nostr: { - relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '["wss://relay.damus.io", "wss://nos.lol"]') - }, - auth: { - sessionTimeout: 24 * 60 * 60 * 1000, - }, - pwa: { - autoPrompt: true - }, - imageUpload: { - baseUrl: import.meta.env.VITE_PICTRS_BASE_URL || 'https://img.mydomain.com', - maxSizeMB: 10, - acceptedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'] - } - } - }, - chat: { - name: 'chat', - enabled: true, - lazy: false, - config: { - maxMessages: 500, - autoScroll: true, - showTimestamps: true, - notifications: { - enabled: true, - soundEnabled: false, - wildcardSupport: true - } - } - }, - }, - - features: { - pwa: true, - pushNotifications: true, - electronApp: false, - developmentMode: import.meta.env.DEV - } -} - -export default appConfig diff --git a/src/chat-app/app.ts b/src/chat-app/app.ts deleted file mode 100644 index d4b2573..0000000 --- a/src/chat-app/app.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { createApp } from 'vue' -import { createRouter, createWebHistory } from 'vue-router' -import { createPinia } from 'pinia' -import { pluginManager } from '@/core/plugin-manager' -import { eventBus } from '@/core/event-bus' -import { container } from '@/core/di-container' - -import appConfig from './app.config' -import baseModule from '@/modules/base' -import chatModule from '@/modules/chat' - -import App from './App.vue' - -import '@/assets/index.css' -import { i18n, changeLocale, type AvailableLocale } from '@/i18n' -import { installStrictAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers' -import { acceptTokenFromUrl } from '@/lib/url-token' - -export async function createAppInstance() { - console.log('Starting Chat app...') - - acceptTokenFromUrl('Chat') - - const app = createApp(App) - - const moduleRoutes = [ - ...baseModule.routes || [], - ...chatModule.routes || [], - ].filter(Boolean) - - const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes: [ - { - path: '/', - redirect: '/chat' - }, - { - path: '/login', - name: 'login', - component: import.meta.env.VITE_DEMO_MODE === 'true' - ? () => import('@/pages/LoginDemo.vue') - : () => import('@/pages/Login.vue'), - meta: { requiresAuth: false } - }, - ...moduleRoutes, - catchAllRoute, - ] - }) - - // Chat has no public view — every non-login route requires auth. - installStrictAuthGuard(router) - - const pinia = createPinia() - - app.use(router) - app.use(pinia) - app.use(i18n) - - const defaultLocale = import.meta.env.VITE_DEFAULT_LOCALE as AvailableLocale | undefined - if (defaultLocale && !localStorage.getItem('user-locale')) { - await changeLocale(defaultLocale) - } - - pluginManager.init(app, router) - - const moduleRegistrations = [] - - if (appConfig.modules.base.enabled) { - moduleRegistrations.push( - pluginManager.register(baseModule, appConfig.modules.base) - ) - } - - if (appConfig.modules.chat?.enabled) { - moduleRegistrations.push( - pluginManager.register(chatModule, appConfig.modules.chat) - ) - } - - 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() - markAuthReady(auth) - - app.config.errorHandler = (err, _vm, info) => { - console.error('Global error:', err, info) - eventBus.emit('app:error', { error: err, info }, 'app') - } - - if (appConfig.features.developmentMode) { - ;(window as any).__pluginManager = pluginManager - ;(window as any).__eventBus = eventBus - ;(window as any).__container = container - } - - console.log('Chat app initialized') - return { app, router } -} - -export async function startApp() { - try { - const { app } = await createAppInstance() - app.mount('#app') - console.log('Chat app started!') - eventBus.emit('app:started', {}, 'app') - } catch (error) { - console.error('Failed to start Chat app:', error) - document.getElementById('app')!.innerHTML = ` -
-

Failed to Start

-

${error instanceof Error ? error.message : 'Unknown error'}

-

Please refresh the page.

-
- ` - } -} diff --git a/src/chat-app/main.ts b/src/chat-app/main.ts deleted file mode 100644 index 7493491..0000000 --- a/src/chat-app/main.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { startApp } from './app' -import { registerSW } from 'virtual:pwa-register' -import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup' -import 'vue-sonner/style.css' - -cleanupStaleDevServiceWorkers() - -const intervalMS = 60 * 60 * 1000 -registerSW({ - onRegistered(r) { - r && setInterval(() => { - r.update() - }, intervalMS) - }, - onOfflineReady() { - console.log('Chat app ready to work offline') - } -}) - -startApp() diff --git a/src/composables/useLocale.ts b/src/composables/useLocale.ts index c53fc8e..e77568a 100644 --- a/src/composables/useLocale.ts +++ b/src/composables/useLocale.ts @@ -27,7 +27,9 @@ export function useLocale() { const flagMap: Record = { 'en': '🇬🇧', 'es': '🇪🇸', - 'fr': '🇫🇷' + 'fr': '🇫🇷', + 'de': '🇩🇪', + 'zh': '🇨🇳' } return flagMap[locale] || '🌐' } diff --git a/src/forum-app/App.vue b/src/forum-app/App.vue deleted file mode 100644 index c8c1abb..0000000 --- a/src/forum-app/App.vue +++ /dev/null @@ -1,104 +0,0 @@ - - - diff --git a/src/forum-app/app.config.ts b/src/forum-app/app.config.ts deleted file mode 100644 index 2860abc..0000000 --- a/src/forum-app/app.config.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { AppConfig } from '@/core/types' - -/** - * Standalone Forum app configuration. - * Only enables base + forum modules. - */ -export const appConfig: AppConfig = { - modules: { - base: { - name: 'base', - enabled: true, - lazy: false, - config: { - nostr: { - relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '["wss://relay.damus.io", "wss://nos.lol"]') - }, - auth: { - sessionTimeout: 24 * 60 * 60 * 1000, - }, - pwa: { - autoPrompt: true - }, - imageUpload: { - baseUrl: import.meta.env.VITE_PICTRS_BASE_URL || 'https://img.mydomain.com', - maxSizeMB: 10, - acceptedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'] - } - } - }, - forum: { - name: 'forum', - enabled: true, - lazy: false, - config: { - maxSubmissions: 50, - corsProxyUrl: import.meta.env.VITE_CORS_PROXY_URL || '', - adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]') - } - }, - }, - - features: { - pwa: true, - pushNotifications: true, - electronApp: false, - developmentMode: import.meta.env.DEV - } -} - -export default appConfig diff --git a/src/forum-app/app.ts b/src/forum-app/app.ts deleted file mode 100644 index b1e1d4c..0000000 --- a/src/forum-app/app.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { createApp } from 'vue' -import { createRouter, createWebHistory } from 'vue-router' -import { createPinia } from 'pinia' -import { pluginManager } from '@/core/plugin-manager' -import { eventBus } from '@/core/event-bus' -import { container } from '@/core/di-container' - -import appConfig from './app.config' -import baseModule from '@/modules/base' -import forumModule from '@/modules/forum' - -import App from './App.vue' - -import '@/assets/index.css' -import { i18n, changeLocale, type AvailableLocale } from '@/i18n' -import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers' -import { acceptTokenFromUrl } from '@/lib/url-token' - -export async function createAppInstance() { - console.log('Starting Forum app...') - - acceptTokenFromUrl('Forum') - - const app = createApp(App) - - const moduleRoutes = [ - ...baseModule.routes || [], - ...forumModule.routes || [], - ].filter(Boolean) - - const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes: [ - { - path: '/', - redirect: '/forum' - }, - { - path: '/login', - name: 'login', - component: import.meta.env.VITE_DEMO_MODE === 'true' - ? () => import('@/pages/LoginDemo.vue') - : () => import('@/pages/Login.vue'), - meta: { requiresAuth: false } - }, - ...moduleRoutes, - catchAllRoute, - ] - }) - - installLenientAuthGuard(router) - - const pinia = createPinia() - - app.use(router) - app.use(pinia) - app.use(i18n) - - const defaultLocale = import.meta.env.VITE_DEFAULT_LOCALE as AvailableLocale | undefined - if (defaultLocale && !localStorage.getItem('user-locale')) { - await changeLocale(defaultLocale) - } - - pluginManager.init(app, router) - - const moduleRegistrations = [] - - if (appConfig.modules.base.enabled) { - moduleRegistrations.push( - pluginManager.register(baseModule, appConfig.modules.base) - ) - } - - if (appConfig.modules.forum?.enabled) { - moduleRegistrations.push( - pluginManager.register(forumModule, appConfig.modules.forum) - ) - } - - 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() - markAuthReady(auth) - - app.config.errorHandler = (err, _vm, info) => { - console.error('Global error:', err, info) - eventBus.emit('app:error', { error: err, info }, 'app') - } - - if (appConfig.features.developmentMode) { - ;(window as any).__pluginManager = pluginManager - ;(window as any).__eventBus = eventBus - ;(window as any).__container = container - } - - console.log('Forum app initialized') - return { app, router } -} - -export async function startApp() { - try { - const { app } = await createAppInstance() - app.mount('#app') - console.log('Forum app started!') - eventBus.emit('app:started', {}, 'app') - } catch (error) { - console.error('Failed to start Forum app:', error) - document.getElementById('app')!.innerHTML = ` -
-

Failed to Start

-

${error instanceof Error ? error.message : 'Unknown error'}

-

Please refresh the page.

-
- ` - } -} diff --git a/src/forum-app/main.ts b/src/forum-app/main.ts deleted file mode 100644 index 8b7df15..0000000 --- a/src/forum-app/main.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { startApp } from './app' -import { registerSW } from 'virtual:pwa-register' -import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup' -import 'vue-sonner/style.css' - -cleanupStaleDevServiceWorkers() - -const intervalMS = 60 * 60 * 1000 -registerSW({ - onRegistered(r) { - r && setInterval(() => { - r.update() - }, intervalMS) - }, - onOfflineReady() { - console.log('Forum app ready to work offline') - } -}) - -startApp() diff --git a/src/i18n/index.ts b/src/i18n/index.ts index 90f7979..d57cae0 100644 --- a/src/i18n/index.ts +++ b/src/i18n/index.ts @@ -5,7 +5,7 @@ import { useStorage } from '@vueuse/core' import en from './locales/en' // Define available locales -export const AVAILABLE_LOCALES = ['en', 'es', 'fr'] as const +export const AVAILABLE_LOCALES = ['en', 'es', 'fr', 'de', 'zh'] as const export type AvailableLocale = typeof AVAILABLE_LOCALES[number] // Type for our messages diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 3e1a597..9b77759 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -168,15 +168,6 @@ const messages: LocaleMessages = { notAvailable: 'Income submission is not yet available. This feature is coming soon.', }, }, - market: { - auth: { - loginPrompt: 'Log in to place your order', - logIn: 'Log in', - logInToCheckout: 'Log in to checkout', - nostrKeyRequired: 'A Nostr identity is required', - nostrKeyDescription: 'Configure your Nostr public key in Profile settings to place orders.', - }, - }, dateTimeFormats: { short: { year: 'numeric', diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 53a7d40..82f0816 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -168,15 +168,6 @@ const messages: LocaleMessages = { notAvailable: 'El registro de ingresos a\u00fan no est\u00e1 disponible. Esta funci\u00f3n llegar\u00e1 pronto.', }, }, - market: { - auth: { - loginPrompt: 'Inicia sesi\u00f3n para realizar tu pedido', - logIn: 'Iniciar sesi\u00f3n', - logInToCheckout: 'Iniciar sesi\u00f3n para finalizar compra', - nostrKeyRequired: 'Se requiere una identidad Nostr', - nostrKeyDescription: 'Configura tu clave p\u00fablica Nostr en los ajustes del perfil para realizar pedidos.', - }, - }, dateTimeFormats: { short: { year: 'numeric', diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 5f11684..b4b4c08 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -168,15 +168,6 @@ const messages: LocaleMessages = { notAvailable: 'La saisie de revenus n\u2019est pas encore disponible. Cette fonctionnalit\u00e9 arrive bient\u00f4t.', }, }, - market: { - auth: { - loginPrompt: 'Connectez-vous pour passer commande', - logIn: 'Se connecter', - logInToCheckout: 'Se connecter pour commander', - nostrKeyRequired: 'Une identit\u00e9 Nostr est requise', - nostrKeyDescription: 'Configurez votre cl\u00e9 publique Nostr dans les param\u00e8tres du profil pour passer commande.', - }, - }, dateTimeFormats: { short: { year: 'numeric', diff --git a/src/i18n/types.ts b/src/i18n/types.ts index d81753a..ddd20c6 100644 --- a/src/i18n/types.ts +++ b/src/i18n/types.ts @@ -144,16 +144,6 @@ export interface LocaleMessages { notAvailable: string } } - // Market module - market?: { - auth: { - loginPrompt: string - logIn: string - logInToCheckout: string - nostrKeyRequired: string - nostrKeyDescription: string - } - } // Add date/time formats dateTimeFormats: { short: { diff --git a/src/lib/api/lnbits.ts b/src/lib/api/lnbits.ts index c837861..1166cc8 100644 --- a/src/lib/api/lnbits.ts +++ b/src/lib/api/lnbits.ts @@ -195,35 +195,6 @@ export class LnbitsAPI extends BaseService { return !!this.accessToken } - /** - * Server-validate a token and adopt it if valid (issue #36). - * - * Called by AuthService.checkAuth() when a pending URL-supplied token is - * found in localStorage. We can't trust the token until the server has - * confirmed it represents a real session, so: - * 1. Temporarily set the candidate token on the API client - * 2. Try getCurrentUser() with it - * 3. On success → persist to AUTH_TOKEN_KEY, return the user - * 4. On failure → restore the previous token (if any), return null - * - * The pending token is the caller's responsibility to remove from - * localStorage afterwards. - */ - async tryAdoptToken(candidateToken: string): Promise { - const previousToken = this.accessToken - this.accessToken = candidateToken - try { - const user = await this.getCurrentUser() - // Server confirmed — persist for future page loads - setAuthToken(candidateToken) - return user - } catch (err) { - console.warn('[LnbitsAPI] Pending URL token rejected by server:', err) - this.accessToken = previousToken - return null - } - } - getAccessToken(): string | null { return this.accessToken } diff --git a/src/lib/config/lnbits.ts b/src/lib/config/lnbits.ts index dec6c8e..5d6ce0c 100644 --- a/src/lib/config/lnbits.ts +++ b/src/lib/config/lnbits.ts @@ -13,11 +13,6 @@ export const LNBITS_CONFIG = { // Auth token storage key AUTH_TOKEN_KEY: 'lnbits_access_token', - // Transient key for tokens received via ?token=… URL params. They live here - // until validateAndAdoptPendingToken() server-checks them; only validated - // tokens get promoted to AUTH_TOKEN_KEY. See issue #36. - PENDING_AUTH_TOKEN_KEY: 'lnbits_pending_token', - // User storage key USER_STORAGE_KEY: 'lnbits_user_data' } @@ -47,20 +42,4 @@ export function setAuthToken(token: string): void { // Helper function to remove auth token from storage export function removeAuthToken(): void { localStorage.removeItem(LNBITS_CONFIG.AUTH_TOKEN_KEY) -} - -// Pending token (URL-supplied, unvalidated) helpers. -// Pending tokens land here from acceptTokenFromUrl() and only get promoted -// to the real AUTH_TOKEN_KEY after server validation. -export function getPendingAuthToken(): string | null { - return localStorage.getItem(LNBITS_CONFIG.PENDING_AUTH_TOKEN_KEY) -} - -export function setPendingAuthToken(token: string): void { - localStorage.setItem(LNBITS_CONFIG.PENDING_AUTH_TOKEN_KEY, token) -} - -export function removePendingAuthToken(): void { - localStorage.removeItem(LNBITS_CONFIG.PENDING_AUTH_TOKEN_KEY) -} - +} diff --git a/src/lib/dev-sw-cleanup.ts b/src/lib/dev-sw-cleanup.ts deleted file mode 100644 index fd3de93..0000000 --- a/src/lib/dev-sw-cleanup.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Unregister any service worker that was registered on this origin during - * a previous dev session (when VitePWA's devOptions.enabled was true). - * - * Once devOptions.enabled was turned off, Vite stopped registering SWs in - * dev — but the browser keeps the previously-registered SWs alive across - * server restarts. They then intercept navigation and serve cached, often - * stale, bundles. This call clears them out at app boot. - * - * Production builds skip this entirely so the legitimate SW from - * `registerSW()` survives. - */ -export async function cleanupStaleDevServiceWorkers(): Promise { - if (!import.meta.env.DEV) return - if (!('serviceWorker' in navigator)) return - - try { - const regs = await navigator.serviceWorker.getRegistrations() - if (regs.length === 0) return - - console.warn( - `[dev-sw-cleanup] Unregistering ${regs.length} stale service worker(s) from a previous dev session.` - ) - await Promise.all(regs.map(r => r.unregister())) - - // Also clear any cache the dev SW left behind. - if ('caches' in window) { - const keys = await caches.keys() - await Promise.all(keys.map(k => caches.delete(k))) - } - - // Reload once so the next request hits the network instead of the - // about-to-be-removed SW. Guard with a sessionStorage flag so we don't - // loop on browsers that take an extra tick to release the controller. - if (!sessionStorage.getItem('dev-sw-cleanup-reloaded')) { - sessionStorage.setItem('dev-sw-cleanup-reloaded', '1') - window.location.reload() - } - } catch (err) { - console.warn('[dev-sw-cleanup] failed to unregister:', err) - } -} diff --git a/src/lib/router-helpers.ts b/src/lib/router-helpers.ts deleted file mode 100644 index c23bf9d..0000000 --- a/src/lib/router-helpers.ts +++ /dev/null @@ -1,79 +0,0 @@ -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 AuthUserLike = { value: { pubkey?: string } | null } -type AuthLike = { - isAuthenticated: { value: boolean } - // Populated after server-validated getCurrentUser() in auth.checkAuth(). - // Guards require BOTH isAuthenticated and a user with a pubkey — token - // presence alone is not enough (issue #36). - currentUser: AuthUserLike -} - -let resolveAuth!: (a: AuthLike) => void -const authReady: Promise = new Promise(r => { resolveAuth = r }) - -export function markAuthReady(auth: AuthLike): void { - resolveAuth(auth) -} - -/** - * Belt-and-suspenders auth check: token presence in localStorage isn't - * sufficient — the server must have confirmed the token represents a real - * session, which is signalled by currentUser being populated with a pubkey. - */ -function isFullyAuthed(auth: AuthLike): boolean { - return auth.isAuthenticated.value && !!auth.currentUser?.value?.pubkey -} - -/** - * 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 - const authed = isFullyAuthed(auth) - if (to.path === '/login') { - return authed ? '/' : true - } - return authed ? 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 - const authed = isFullyAuthed(auth) - if (requiresAuth && !authed) return '/login' - if (to.path === '/login' && authed) 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/lib/url-token.ts b/src/lib/url-token.ts deleted file mode 100644 index 01ee5a8..0000000 --- a/src/lib/url-token.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { setPendingAuthToken } from '@/lib/config/lnbits' - -/** - * Cross-subdomain auth relay (issue #36): pull `?token=…` off the URL into - * the pending-token slot in localStorage, then strip it from history so it - * doesn't bleed into bookmarks or referrers. - * - * The token is NOT promoted to the real auth-token slot here. AuthService - * .checkAuth() server-validates it via lnbitsAPI.tryAdoptToken() and only - * persists it if the LNbits backend confirms it represents a real session. - * - * Call this synchronously at app boot, before createApp(), so the URL is - * cleaned before vue-router has a chance to read it. The pending token sits - * in localStorage until auth.initialize() picks it up later in the same - * page load. - */ -export function acceptTokenFromUrl(appName: string): void { - const params = new URLSearchParams(window.location.search) - const token = params.get('token') - if (!token) return - setPendingAuthToken(token) - params.delete('token') - const clean = params.toString() - const newUrl = window.location.pathname + (clean ? `?${clean}` : '') + window.location.hash - window.history.replaceState({}, '', newUrl) - console.log(`[${appName}] URL token captured for server validation`) -} diff --git a/src/main.ts b/src/main.ts index a12e2a8..ee1eff7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,8 @@ // New modular application entry point import { startApp } from './app' import { registerSW } from 'virtual:pwa-register' -import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup' import 'vue-sonner/style.css' -// Clean up any leftover dev-mode service workers from a previous session -cleanupStaleDevServiceWorkers() - // Simple periodic service worker updates const intervalMS = 60 * 60 * 1000 // 1 hour registerSW({ diff --git a/src/market-app/App.vue b/src/market-app/App.vue deleted file mode 100644 index 7d40951..0000000 --- a/src/market-app/App.vue +++ /dev/null @@ -1,114 +0,0 @@ - - - diff --git a/src/market-app/app.config.ts b/src/market-app/app.config.ts deleted file mode 100644 index e222aa4..0000000 --- a/src/market-app/app.config.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { AppConfig } from '@/core/types' - -/** - * Standalone Market app configuration. - * Only enables base + market modules. - */ -export const appConfig: AppConfig = { - modules: { - base: { - name: 'base', - enabled: true, - lazy: false, - config: { - nostr: { - relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '["wss://relay.damus.io", "wss://nos.lol"]') - }, - auth: { - sessionTimeout: 24 * 60 * 60 * 1000, - }, - pwa: { - autoPrompt: true - }, - imageUpload: { - baseUrl: import.meta.env.VITE_PICTRS_BASE_URL || 'https://img.mydomain.com', - maxSizeMB: 10, - acceptedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'] - } - } - }, - market: { - name: 'market', - enabled: true, - lazy: false, - config: { - defaultCurrency: 'sats', - paymentTimeout: 300000, - maxOrderHistory: 50, - apiConfig: { - baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000' - } - } - }, - }, - - features: { - pwa: true, - pushNotifications: true, - electronApp: false, - developmentMode: import.meta.env.DEV - } -} - -export default appConfig diff --git a/src/market-app/app.ts b/src/market-app/app.ts deleted file mode 100644 index 103ce53..0000000 --- a/src/market-app/app.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { createApp } from 'vue' -import { createRouter, createWebHistory } from 'vue-router' -import { createPinia } from 'pinia' -import { pluginManager } from '@/core/plugin-manager' -import { eventBus } from '@/core/event-bus' -import { container } from '@/core/di-container' - -import appConfig from './app.config' -import baseModule from '@/modules/base' -import marketModule from '@/modules/market' - -import App from './App.vue' - -import '@/assets/index.css' -import { i18n, changeLocale, type AvailableLocale } from '@/i18n' -import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers' -import { acceptTokenFromUrl } from '@/lib/url-token' - -export async function createAppInstance() { - console.log('Starting Market app...') - - acceptTokenFromUrl('Market') - - const app = createApp(App) - - const moduleRoutes = [ - ...baseModule.routes || [], - ...marketModule.routes || [], - ].filter(Boolean) - - const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes: [ - { - path: '/', - redirect: '/market' - }, - { - path: '/login', - name: 'login', - component: import.meta.env.VITE_DEMO_MODE === 'true' - ? () => import('@/pages/LoginDemo.vue') - : () => import('@/pages/Login.vue'), - meta: { requiresAuth: false } - }, - ...moduleRoutes, - catchAllRoute, - ] - }) - - installLenientAuthGuard(router) - - const pinia = createPinia() - - app.use(router) - app.use(pinia) - app.use(i18n) - - const defaultLocale = import.meta.env.VITE_DEFAULT_LOCALE as AvailableLocale | undefined - if (defaultLocale && !localStorage.getItem('user-locale')) { - await changeLocale(defaultLocale) - } - - pluginManager.init(app, router) - - const moduleRegistrations = [] - - if (appConfig.modules.base.enabled) { - moduleRegistrations.push( - pluginManager.register(baseModule, appConfig.modules.base) - ) - } - - if (appConfig.modules.market?.enabled) { - moduleRegistrations.push( - pluginManager.register(marketModule, appConfig.modules.market) - ) - } - - 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() - markAuthReady(auth) - - app.config.errorHandler = (err, _vm, info) => { - console.error('Global error:', err, info) - eventBus.emit('app:error', { error: err, info }, 'app') - } - - if (appConfig.features.developmentMode) { - ;(window as any).__pluginManager = pluginManager - ;(window as any).__eventBus = eventBus - ;(window as any).__container = container - } - - console.log('Market app initialized') - return { app, router } -} - -export async function startApp() { - try { - const { app } = await createAppInstance() - app.mount('#app') - console.log('Market app started!') - eventBus.emit('app:started', {}, 'app') - } catch (error) { - console.error('Failed to start Market app:', error) - document.getElementById('app')!.innerHTML = ` -
-

Failed to Start

-

${error instanceof Error ? error.message : 'Unknown error'}

-

Please refresh the page.

-
- ` - } -} diff --git a/src/market-app/main.ts b/src/market-app/main.ts deleted file mode 100644 index 4965337..0000000 --- a/src/market-app/main.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { startApp } from './app' -import { registerSW } from 'virtual:pwa-register' -import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup' -import 'vue-sonner/style.css' - -cleanupStaleDevServiceWorkers() - -const intervalMS = 60 * 60 * 1000 -registerSW({ - onRegistered(r) { - r && setInterval(() => { - r.update() - }, intervalMS) - }, - onOfflineReady() { - console.log('Market app ready to work offline') - } -}) - -startApp() diff --git a/src/modules/base/auth/auth-service.ts b/src/modules/base/auth/auth-service.ts index 7b4b6fe..9f64931 100644 --- a/src/modules/base/auth/auth-service.ts +++ b/src/modules/base/auth/auth-service.ts @@ -5,7 +5,6 @@ import { eventBus } from '@/core/event-bus' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { LoginCredentials, RegisterData, User } from '@/lib/api/lnbits' import type { NostrMetadataService } from '../nostr/nostr-metadata-service' -import { getPendingAuthToken, removePendingAuthToken } from '@/lib/config/lnbits' export class AuthService extends BaseService { // Service metadata @@ -50,28 +49,6 @@ export class AuthService extends BaseService { } async checkAuth(): Promise { - // Pending URL-supplied token (from acceptTokenFromUrl in app shells). - // Validate server-side before promoting to the real auth-token slot — - // see issue #36. Always remove the pending entry whether validation - // succeeds or fails so it can't recur on later boots. - const pending = getPendingAuthToken() - if (pending) { - removePendingAuthToken() - this.isLoading.value = true - try { - const adopted = await this.lnbitsAPI.tryAdoptToken(pending) - if (adopted) { - this.user.value = adopted - this.isAuthenticated.value = true - this.debug(`Adopted pending URL token for ${adopted.username || adopted.id}`) - return true - } - this.debug('Pending URL token rejected — falling through to existing token') - } finally { - this.isLoading.value = false - } - } - if (!this.lnbitsAPI.isAuthenticated()) { this.debug('No auth token found - user needs to login') this.isAuthenticated.value = false @@ -82,14 +59,14 @@ export class AuthService extends BaseService { try { this.isLoading.value = true const userData = await this.lnbitsAPI.getCurrentUser() - + this.user.value = userData this.isAuthenticated.value = true - + this.debug(`User authenticated: ${userData.username || userData.id} (${userData.pubkey?.slice(0, 8)})`) - + return true - + } catch (error) { this.handleError(error, 'checkAuth') this.isAuthenticated.value = false diff --git a/src/modules/base/components/ProfileSettings.vue b/src/modules/base/components/ProfileSettings.vue index fed1828..d1dab22 100644 --- a/src/modules/base/components/ProfileSettings.vue +++ b/src/modules/base/components/ProfileSettings.vue @@ -147,34 +147,6 @@ Use the "Broadcast to Nostr" button to manually re-broadcast your profile.

- - - -
- - - - - - - Log out of {{ user?.username || 'your account' }}? - - You'll need to sign in again to access your wallet, post in the - forum, place orders, or use any feature that needs your account. - - - - Cancel - - Log out - - - - -
@@ -196,28 +168,14 @@ import { FormMessage, } from '@/components/ui/form' import ImageUpload from './ImageUpload.vue' -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from '@/components/ui/alert-dialog' -import { LogOut } from 'lucide-vue-next' import { useAuth } from '@/composables/useAuthService' -import { useRouter } from 'vue-router' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import type { ImageUploadService } from '../services/ImageUploadService' import type { NostrMetadataService } from '../nostr/nostr-metadata-service' import { useToast } from '@/core/composables/useToast' // Services -const { user, updateProfile, logout } = useAuth() -const router = useRouter() +const { user, updateProfile } = useAuth() const imageService = injectService(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE) const metadataService = injectService(SERVICE_TOKENS.NOSTR_METADATA_SERVICE) const toast = useToast() @@ -364,17 +322,4 @@ const broadcastMetadata = async () => { isBroadcasting.value = false } } - -// Log out + redirect to /login on this app's origin. -const onLogout = async () => { - try { - await logout() - toast.success('Logged out') - router.push('/login') - } catch (error) { - const errorMessage = error instanceof Error ? error.message : 'Failed to log out' - console.error('Error logging out:', error) - toast.error(`Logout failed: ${errorMessage}`) - } -} diff --git a/src/modules/forum/views/ForumListPage.vue b/src/modules/forum/views/ForumListPage.vue deleted file mode 100644 index 9c9246b..0000000 --- a/src/modules/forum/views/ForumListPage.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - diff --git a/src/modules/forum/components/SortTabs.vue b/src/modules/links/components/SortTabs.vue similarity index 100% rename from src/modules/forum/components/SortTabs.vue rename to src/modules/links/components/SortTabs.vue diff --git a/src/modules/forum/components/SubmissionComment.vue b/src/modules/links/components/SubmissionComment.vue similarity index 100% rename from src/modules/forum/components/SubmissionComment.vue rename to src/modules/links/components/SubmissionComment.vue diff --git a/src/modules/forum/components/SubmissionDetail.vue b/src/modules/links/components/SubmissionDetail.vue similarity index 100% rename from src/modules/forum/components/SubmissionDetail.vue rename to src/modules/links/components/SubmissionDetail.vue diff --git a/src/modules/forum/components/SubmissionList.vue b/src/modules/links/components/SubmissionList.vue similarity index 100% rename from src/modules/forum/components/SubmissionList.vue rename to src/modules/links/components/SubmissionList.vue diff --git a/src/modules/forum/components/SubmissionRow.vue b/src/modules/links/components/SubmissionRow.vue similarity index 100% rename from src/modules/forum/components/SubmissionRow.vue rename to src/modules/links/components/SubmissionRow.vue diff --git a/src/modules/forum/components/SubmissionThumbnail.vue b/src/modules/links/components/SubmissionThumbnail.vue similarity index 100% rename from src/modules/forum/components/SubmissionThumbnail.vue rename to src/modules/links/components/SubmissionThumbnail.vue diff --git a/src/modules/forum/components/SubmitComposer.vue b/src/modules/links/components/SubmitComposer.vue similarity index 100% rename from src/modules/forum/components/SubmitComposer.vue rename to src/modules/links/components/SubmitComposer.vue diff --git a/src/modules/forum/components/VoteControls.vue b/src/modules/links/components/VoteControls.vue similarity index 100% rename from src/modules/forum/components/VoteControls.vue rename to src/modules/links/components/VoteControls.vue diff --git a/src/modules/forum/composables/useSubmissions.ts b/src/modules/links/composables/useSubmissions.ts similarity index 100% rename from src/modules/forum/composables/useSubmissions.ts rename to src/modules/links/composables/useSubmissions.ts diff --git a/src/modules/forum/index.ts b/src/modules/links/index.ts similarity index 76% rename from src/modules/forum/index.ts rename to src/modules/links/index.ts index c8b645b..ebf7b74 100644 --- a/src/modules/forum/index.ts +++ b/src/modules/links/index.ts @@ -7,8 +7,8 @@ import { LinkPreviewService } from './services/LinkPreviewService' import SubmissionList from './components/SubmissionList.vue' import SubmitComposer from './components/SubmitComposer.vue' -export const forumModule: ModulePlugin = { - name: 'forum', +export const linksModule: ModulePlugin = { + name: 'links', version: '1.0.0', dependencies: ['base'], @@ -25,12 +25,6 @@ export const forumModule: ModulePlugin = { ], routes: [ - { - path: '/forum', - name: 'forum', - component: () => import('./views/ForumListPage.vue'), - meta: { title: 'Forum', requiresAuth: false } - }, { path: '/submission/:id', name: 'submission-detail', @@ -46,16 +40,16 @@ export const forumModule: ModulePlugin = { ], async install(app: App) { - console.log('forum module: Starting installation...') + console.log('links module: Starting installation...') const submissionService = new SubmissionService() const linkPreviewService = new LinkPreviewService() container.provide(SERVICE_TOKENS.SUBMISSION_SERVICE, submissionService) container.provide(SERVICE_TOKENS.LINK_PREVIEW_SERVICE, linkPreviewService) - console.log('forum module: Services registered in DI container') + console.log('links module: Services registered in DI container') - console.log('forum module: Initializing services...') + console.log('links module: Initializing services...') await Promise.all([ submissionService.initialize({ waitForDependencies: true, @@ -66,10 +60,10 @@ export const forumModule: ModulePlugin = { maxRetries: 3 }) ]) - console.log('forum module: Services initialized') + console.log('links module: Services initialized') app.component('SubmissionList', SubmissionList) - console.log('forum module: Installation complete') + console.log('links module: Installation complete') }, components: { @@ -80,4 +74,4 @@ export const forumModule: ModulePlugin = { composables: {} } -export default forumModule +export default linksModule diff --git a/src/modules/forum/services/LinkPreviewService.ts b/src/modules/links/services/LinkPreviewService.ts similarity index 100% rename from src/modules/forum/services/LinkPreviewService.ts rename to src/modules/links/services/LinkPreviewService.ts diff --git a/src/modules/forum/services/SubmissionService.ts b/src/modules/links/services/SubmissionService.ts similarity index 99% rename from src/modules/forum/services/SubmissionService.ts rename to src/modules/links/services/SubmissionService.ts index f9ad31b..cd4a808 100644 --- a/src/modules/forum/services/SubmissionService.ts +++ b/src/modules/links/services/SubmissionService.ts @@ -369,7 +369,7 @@ export class SubmissionService extends BaseService { this._submissions.set(submission.id, submissionWithMeta) // Emit event - eventBus.emit('submission:new', { submission: submissionWithMeta }, 'forum') + eventBus.emit('submission:new', { submission: submissionWithMeta }, 'links') } /** diff --git a/src/modules/forum/types/index.ts b/src/modules/links/types/index.ts similarity index 100% rename from src/modules/forum/types/index.ts rename to src/modules/links/types/index.ts diff --git a/src/modules/forum/types/submission.ts b/src/modules/links/types/submission.ts similarity index 100% rename from src/modules/forum/types/submission.ts rename to src/modules/links/types/submission.ts diff --git a/src/modules/forum/views/SubmissionDetailPage.vue b/src/modules/links/views/SubmissionDetailPage.vue similarity index 100% rename from src/modules/forum/views/SubmissionDetailPage.vue rename to src/modules/links/views/SubmissionDetailPage.vue diff --git a/src/modules/forum/views/SubmitPage.vue b/src/modules/links/views/SubmitPage.vue similarity index 100% rename from src/modules/forum/views/SubmitPage.vue rename to src/modules/links/views/SubmitPage.vue diff --git a/src/modules/market/components/CartButton.vue b/src/modules/market/components/CartButton.vue new file mode 100644 index 0000000..6377564 --- /dev/null +++ b/src/modules/market/components/CartButton.vue @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/src/modules/market/composables/useMarket.ts b/src/modules/market/composables/useMarket.ts index 62c66da..cadb14b 100644 --- a/src/modules/market/composables/useMarket.ts +++ b/src/modules/market/composables/useMarket.ts @@ -3,7 +3,7 @@ import { useMarketStore } from '../stores/market' import { injectService, SERVICE_TOKENS } from '@/core/di-container' import { config } from '@/lib/config' import type { NostrmarketService } from '../services/nostrmarketService' -import { nip59 } from 'nostr-tools' +import { nip04 } from 'nostr-tools' import { useAsyncOperation } from '@/core/composables/useAsyncOperation' import { auth } from '@/composables/useAuthService' @@ -14,32 +14,6 @@ const MARKET_EVENT_KINDS = { PRODUCT: 30018 } as const -/** - * Resolve a product's parent stall id from the event. - * - * NIP-15 lists `stall_id` inside the JSON `content`, but some publishers - * (older nostrmarket builds, third-party clients) only emit the parent - * reference via an `a` tag of the form - * ["a", "30017::"] - * - * Read content first, then fall back to the tag, then a sentinel that won't - * match any real stall. Returning the tag form prevents "Unknown Stall" - * from sticking when the JSON omits the field. - */ -function resolveStallId(event: any, productData: any): string { - if (productData?.stall_id && typeof productData.stall_id === 'string') { - return productData.stall_id - } - const aTag = event.tags?.find( - (t: any) => Array.isArray(t) && t[0] === 'a' && typeof t[1] === 'string' && t[1].startsWith(`${MARKET_EVENT_KINDS.STALL}:`) - ) - if (aTag) { - const parts = aTag[1].split(':') - if (parts[2]) return parts[2] - } - return 'unknown' -} - export function useMarket() { const marketStore = useMarketStore() const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any @@ -54,29 +28,19 @@ export function useMarket() { throw new Error('AuthService not available. Make sure base module is installed.') } - // Subscribe to incoming order gift wraps (NIP-17 / kind 1059) addressed to the user. - // - // The chat service still runs on NIP-04 (kind 4); when it migrates to NIP-17 it - // can take over routing of order DMs the way it does today via setMarketMessageHandler. - // Until then the market subscribes directly so order flows aren't dependent on chat. + // Register market DM handler with chat service (if available) const registerMarketMessageHandler = () => { try { - const userPubkey = authService.user.value?.pubkey || auth.currentUser.value?.pubkey - if (!userPubkey) { - console.log('🛒 No user pubkey available; skipping order gift-wrap subscription') - return + // Try to get the chat service (it might not be available if chat module isn't loaded) + const chatService = (globalThis as any).chatService + if (chatService && chatService.setMarketMessageHandler) { + chatService.setMarketMessageHandler(handleOrderDM) + console.log('🛒 Registered market message handler with chat service') + } else { + console.log('🛒 Chat service not available, market will use its own DM subscription') } - - const unsubscribe = relayHub.subscribe({ - id: `market-orders-${userPubkey.slice(0, 16)}`, - filters: [{ kinds: [1059], '#p': [userPubkey] }], - onEvent: (event: any) => handleOrderDM(event) - }) - console.log('🎁 Subscribed to order gift wraps (kind 1059)') - // unsubscribe is currently not retained; market lifecycle owns this - void unsubscribe } catch (error) { - console.warn('🛒 Failed to subscribe to order gift wraps:', error) + console.log('🛒 Could not register with chat service:', error) } } @@ -100,15 +64,17 @@ export function useMarket() { return 'disconnected' }) - // Load market from naddr (or empty for public browse mode) + // Load market from naddr const loadMarket = async (naddr: string) => { return await marketOperation.execute(async () => { - // Parse naddr (when given) to get market identifier + pubkey. - // Empty naddr + unauth user → public browse mode (no pubkey filter). - const parts = naddr ? naddr.split(':') : [] + // Parse naddr to get market data const marketData = { - identifier: parts[2] || 'default', - pubkey: parts[1] || authService.user.value?.pubkey || '' + identifier: naddr.split(':')[2] || 'default', + pubkey: naddr.split(':')[1] || authService.user.value?.pubkey || '' + } + + if (!marketData.pubkey) { + throw new Error('No pubkey available for market') } await loadMarketData(marketData) @@ -121,30 +87,8 @@ export function useMarket() { // Load market data from Nostr events const loadMarketData = async (marketData: any) => { try { - console.log('🛒 Loading market data for:', { identifier: marketData.identifier, pubkey: marketData.pubkey?.slice(0, 8) || '(public browse)' }) - - // Public browse mode: no curated naddr and no logged-in user. - // Skip the kind 30019 query and use a "Discover" placeholder market; - // loadStalls/loadProducts treat browseAll=true as "no authors filter". - if (!marketData.pubkey) { - const market = { - d: marketData.identifier, - pubkey: '', - relays: config.nostr.relays, - selected: true, - browseAll: true, - opts: { - name: 'Discover', - description: 'Public stalls and products from your relays', - merchants: [], - ui: {} - } - } - marketStore.addMarket(market) - marketStore.setActiveMarket(market) - return - } - + console.log('🛒 Loading market data for:', { identifier: marketData.identifier, pubkey: marketData.pubkey?.slice(0, 8) }) + // Check if we can query events (relays are connected) if (!isConnected.value) { console.log('🛒 Not connected to relays, creating default market') @@ -161,12 +105,12 @@ export function useMarket() { ui: {} } } - + marketStore.addMarket(market) marketStore.setActiveMarket(market) return } - + // Load market data from Nostr events // Fetch market configuration event const events = await relayHub.queryEvents([ @@ -204,11 +148,7 @@ export function useMarket() { relays: config.nostr.relays, selected: true, opts: { - // Logged-in user has no published market event yet — show their - // namespace as "My Market". Avoids leaking VITE_APP_NAME (which - // is the brand of whichever standalone app is bundled, e.g. - // "Sortir" for activities) into the market label. - name: 'My Market', + name: `${import.meta.env.VITE_APP_NAME} Market`, description: 'A communal market to sell your goods', merchants: [], ui: {} @@ -239,7 +179,7 @@ export function useMarket() { } } - // Load stalls from market merchants (or all stalls in public browse mode) + // Load stalls from market merchants const loadStalls = async () => { try { // Get the active market to filter by its merchants @@ -248,20 +188,19 @@ export function useMarket() { return } - const browseAll = (activeMarket as any).browseAll === true - const merchants = [...(activeMarket.opts.merchants || [])] + const merchants = [...(activeMarket.opts.merchants || [])] + + if (merchants.length === 0) { + return + } - if (!browseAll && merchants.length === 0) { - return - } - - // Build filter: in browse-all mode no authors filter; otherwise scope to merchants. - const stallFilter: any = { kinds: [MARKET_EVENT_KINDS.STALL] } - if (!browseAll && merchants.length > 0) { - stallFilter.authors = merchants - } - - const events = await relayHub.queryEvents([stallFilter]) + // Fetch stall events from market merchants only + const events = await relayHub.queryEvents([ + { + kinds: [MARKET_EVENT_KINDS.STALL], + authors: merchants + } + ]) console.log('🛒 Found', events.length, 'stall events for', merchants.length, 'merchants') @@ -306,7 +245,7 @@ export function useMarket() { } } - // Load products from market stalls (or all products in public browse mode) + // Load products from market stalls const loadProducts = async () => { try { const activeMarket = marketStore.activeMarket @@ -314,19 +253,18 @@ export function useMarket() { return } - const browseAll = (activeMarket as any).browseAll === true const merchants = [...(activeMarket.opts.merchants || [])] + if (merchants.length === 0) { + return + } - if (!browseAll && merchants.length === 0) { - return - } - - const productFilter: any = { kinds: [MARKET_EVENT_KINDS.PRODUCT] } - if (!browseAll && merchants.length > 0) { - productFilter.authors = merchants - } - - const events = await relayHub.queryEvents([productFilter]) + // Fetch product events from market merchants + const events = await relayHub.queryEvents([ + { + kinds: [MARKET_EVENT_KINDS.PRODUCT], + authors: merchants + } + ]) console.log('🛒 Found', events.length, 'product events for', merchants.length, 'merchants') @@ -351,7 +289,7 @@ export function useMarket() { try { const productData = JSON.parse(latestEvent.content) - const stallId = resolveStallId(latestEvent, productData) + const stallId = productData.stall_id || 'unknown' // Extract categories from Nostr event tags (standard approach) const categories = latestEvent.tags @@ -433,46 +371,53 @@ export function useMarket() { return null } - // Convert hex string to Uint8Array (browser-compatible) - const hexToUint8Array = (hex: string): Uint8Array => { - const bytes = new Uint8Array(hex.length / 2) - for (let i = 0; i < hex.length; i += 2) { - bytes[i / 2] = parseInt(hex.slice(i, i + 2), 16) - } - return bytes - } - - // Handle incoming order gift wraps (kind 1059) — payment requests, status updates. - // - // The outer event's pubkey is an ephemeral key (NIP-59); the real merchant - // pubkey is on the unwrapped rumor. Content is JSON with a `type` field - // (1 = payment request, 2 = order status update). + // Handle incoming order DMs (payment requests, status updates) const handleOrderDM = async (event: any) => { try { - console.log('🎁 Received order gift wrap:', event.id, '(kind', event.kind + ')') - - const userPrivkey = - authService.user.value?.prvkey ?? auth.currentUser.value?.prvkey - - if (!userPrivkey) { - console.warn('Cannot unwrap gift wrap: no user private key available') + console.log('🔔 Received order-related DM:', event.id, 'from:', event.pubkey.slice(0, 8)) + + // Check both injected auth service AND global auth composable + const hasAuthService = authService.user.value?.prvkey + const hasGlobalAuth = auth.currentUser.value?.prvkey + + const userPrivkey = hasAuthService ? authService.user.value.prvkey : auth.currentUser.value?.prvkey + const userPubkey = hasAuthService ? authService.user.value.pubkey : auth.currentUser.value?.pubkey + + if (!userPrivkey || !userPubkey) { + console.warn('Cannot decrypt DM: no user private key available', { + hasAuthService: !!hasAuthService, + hasGlobalAuth: !!hasGlobalAuth, + authServicePrivkey: !!authService.user.value?.prvkey, + globalAuthPrivkey: !!auth.currentUser.value?.prvkey + }) return } + + console.log('🔐 Market DM decryption auth check:', { + hasAuthService: !!hasAuthService, + hasGlobalAuth: !!hasGlobalAuth, + usingAuthService: !!hasAuthService, + userPubkey: userPubkey.substring(0, 10) + '...' + }) - const prvkeyBytes = hexToUint8Array(userPrivkey) - const rumor = nip59.unwrapEvent(event, prvkeyBytes) - console.log('🔓 Unwrapped rumor from merchant:', rumor.pubkey.slice(0, 10) + '...') + console.log('🔓 Attempting to decrypt DM with private key available') - const messageData = JSON.parse(rumor.content) + // Decrypt the DM content + const decryptedContent = await nip04.decrypt(userPrivkey, event.pubkey, event.content) + console.log('🔓 Decrypted DM content:', decryptedContent) + + // Parse the decrypted content as JSON + const messageData = JSON.parse(decryptedContent) console.log('📨 Parsed message data:', messageData) + // Handle different types of messages switch (messageData.type) { case 1: // Payment request console.log('💰 Processing payment request for order:', messageData.id) await nostrmarketService.handlePaymentRequest(messageData) console.log('✅ Payment request processed successfully') break - case 2: // Order status update + case 2: // Order status update console.log('📦 Processing order status update for order:', messageData.id) await nostrmarketService.handleOrderStatusUpdate(messageData) console.log('✅ Order status update processed successfully') @@ -481,7 +426,7 @@ export function useMarket() { console.log('❓ Unknown message type:', messageData.type) } } catch (error) { - console.error('Failed to handle order gift wrap:', error) + console.error('Failed to handle order DM:', error) } } @@ -544,7 +489,7 @@ export function useMarket() { const productId = event.tags.find((tag: any) => tag[0] === 'd')?.[1] if (productId) { const productData = JSON.parse(event.content) - const stallId = resolveStallId(event, productData) + const stallId = productData.stall_id || 'unknown' // Extract categories from Nostr event tags (standard approach) const categories = event.tags diff --git a/src/modules/market/composables/useMarketStallSelfHeal.ts b/src/modules/market/composables/useMarketStallSelfHeal.ts deleted file mode 100644 index 6a0eb65..0000000 --- a/src/modules/market/composables/useMarketStallSelfHeal.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { useAuth } from '@/composables/useAuthService' -import { injectService, SERVICE_TOKENS } from '@/core/di-container' -import type { NostrmarketAPI } from '../services/nostrmarketAPI' - -const SESSION_FLAG = 'market-stall-self-heal-checked' -const STALL_EVENT_KIND = 30017 - -/** - * Detect-and-recover from the LNbits orphan-stall bug - * (aiolabs/lnbits#10): _create_default_merchant provisions the merchant - * + stall in nostrmarket's internal SQLite but historically never - * published the kind-30017 stall event to relays. The upstream fix is - * already in c0f3743c on aiolabs/lnbits@demo, but it only helps NEW - * signups. Existing accounts whose auto-stall never made it to a relay - * stay orphaned until somebody republishes — which manifests in our - * webapp as "Unknown Stall" on every product authored by them. - * - * This composable runs once per browser session (sessionStorage gate) - * for any logged-in user who lands on the merchant dashboard: - * - * 1. Ask the relay for kind-30017 events authored by their pubkey. - * 2. Ask LNbits for the merchant's known stalls. - * 3. For each stall in (2) whose id isn't represented in (1), PUT the - * stall back to LNbits. The PUT path on the LNbits side already - * calls sign_and_send_to_nostr, so the kind-30017 event lands on - * the relay without any user interaction. - * - * Silent on success. Logs to console.info on republish; console.warn on - * failure. Never toasts — this is supposed to be invisible. - * - * Tracked in aiolabs/webapp#38. - */ -export function useMarketStallSelfHeal() { - const { user } = useAuth() - const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any - const nostrmarketAPI = injectService(SERVICE_TOKENS.NOSTRMARKET_API) as NostrmarketAPI - - async function selfHealOnce(): Promise { - if (sessionStorage.getItem(SESSION_FLAG)) return - - const currentUser = user.value - if (!currentUser?.pubkey) return - - const wallets = (currentUser as any).wallets as Array<{ adminkey?: string; inkey?: string }> | undefined - if (!wallets?.length) return - - const adminWallet = wallets.find(w => w.adminkey) || wallets[0] - if (!adminWallet?.adminkey || !adminWallet?.inkey) return - - // Mark checked early — even on failure we don't want to retry on every - // dashboard mount during the same tab session. - sessionStorage.setItem(SESSION_FLAG, '1') - - if (!relayHub || !nostrmarketAPI) { - console.warn('[market-self-heal] Required services unavailable, skipping') - return - } - - try { - const relayEvents: Array<{ tags?: Array<[string, string?]> }> = await relayHub.queryEvents([ - { kinds: [STALL_EVENT_KIND], authors: [currentUser.pubkey] }, - ]) - const publishedStallIds = new Set() - for (const ev of relayEvents) { - const dTag = ev.tags?.find(t => Array.isArray(t) && t[0] === 'd') - const stallId = dTag?.[1] - if (stallId) publishedStallIds.add(stallId) - } - - const lnbitsStalls = await nostrmarketAPI.getStalls(adminWallet.inkey) - const orphans = lnbitsStalls.filter(s => !publishedStallIds.has(s.id)) - - if (orphans.length === 0) { - console.info( - `[market-self-heal] All ${lnbitsStalls.length} stall(s) have a relay event — no recovery needed.`, - ) - return - } - - console.info( - `[market-self-heal] Republishing ${orphans.length} orphan stall(s):`, - orphans.map(s => `${s.id} (${s.name})`), - ) - - for (const stall of orphans) { - try { - await nostrmarketAPI.updateStall(adminWallet.adminkey, stall) - console.info(`[market-self-heal] Republished ${stall.id} (${stall.name})`) - } catch (err) { - console.warn(`[market-self-heal] Failed to republish ${stall.id}:`, err) - } - } - } catch (err) { - console.warn('[market-self-heal] Self-heal check failed:', err) - } - } - - return { selfHealOnce } -} diff --git a/src/modules/market/services/nostrmarketService.ts b/src/modules/market/services/nostrmarketService.ts index 6598d38..733879e 100644 --- a/src/modules/market/services/nostrmarketService.ts +++ b/src/modules/market/services/nostrmarketService.ts @@ -1,4 +1,4 @@ -import { type EventTemplate, nip59 } from 'nostr-tools' +import { finalizeEvent, type EventTemplate, nip04 } from 'nostr-tools' import { BaseService } from '@/core/base/BaseService' import type { Order } from '@/modules/market/stores/market' @@ -159,17 +159,12 @@ export class NostrmarketService extends BaseService { // Stall and product publishing is now handled by LNbits API endpoints /** - * Publish an order as a NIP-59 gift-wrapped (kind 1059) event to nostrmarket. - * - * The order JSON is placed in an unsigned kind 14 rumor, sealed (kind 13) - * with the customer's key, and wrapped (kind 1059) with an ephemeral key. - * Only the merchant can decrypt the wrap; the public event reveals nothing - * about the sender. + * Publish an order event (kind 4 encrypted DM) to nostrmarket */ async publishOrder(order: Order, merchantPubkey: string): Promise { const { prvkey } = this.getAuth() - - // Convert order to nostrmarket format - matches NIP-15 customer order spec + + // Convert order to nostrmarket format - exactly matching the specification const orderData = { type: 0, // DirectMessageType.CUSTOMER_ORDER id: order.id, @@ -180,37 +175,72 @@ export class NostrmarketService extends BaseService { contact: { name: order.contactInfo?.message || order.contactInfo?.email || 'Unknown', email: order.contactInfo?.email || '' + // Remove phone field - not in nostrmarket specification }, + // Only include address if it's a physical good and address is provided ...(order.shippingZone?.requiresPhysicalShipping && order.contactInfo?.address ? { address: order.contactInfo.address } : {}), shipping_id: order.shippingZone?.id || 'online' } - const rumorTemplate: Partial = { - kind: 14, - tags: [['p', merchantPubkey]], - content: JSON.stringify(orderData), + // Encrypt the message using NIP-04 + console.log('🔐 NIP-04 encryption debug:', { + prvkeyType: typeof prvkey, + prvkeyIsString: typeof prvkey === 'string', + prvkeyLength: prvkey.length, + prvkeySample: prvkey.substring(0, 10) + '...', + merchantPubkeyType: typeof merchantPubkey, + merchantPubkeyLength: merchantPubkey.length, + orderDataString: JSON.stringify(orderData).substring(0, 50) + '...' + }) + + let encryptedContent: string + try { + encryptedContent = await nip04.encrypt(prvkey, merchantPubkey, JSON.stringify(orderData)) + console.log('🔐 NIP-04 encryption successful:', { + encryptedContentLength: encryptedContent.length, + encryptedContentSample: encryptedContent.substring(0, 50) + '...' + }) + } catch (error) { + console.error('🔐 NIP-04 encryption failed:', error) + throw error + } + + const eventTemplate: EventTemplate = { + kind: 4, // Encrypted DM + tags: [['p', merchantPubkey]], // Recipient (merchant) + content: encryptedContent, // Use encrypted content created_at: Math.floor(Date.now() / 1000) } - const prvkeyBytes = this.hexToUint8Array(prvkey) - const giftWrap = nip59.wrapEvent(rumorTemplate, prvkeyBytes, merchantPubkey) - - console.log('🎁 Order gift-wrapped (NIP-17):', { - orderId: order.id, - giftWrapId: giftWrap.id, - kind: giftWrap.kind, - merchantPubkey: merchantPubkey.substring(0, 10) + '...' + console.log('🔧 finalizeEvent debug:', { + prvkeyType: typeof prvkey, + prvkeyIsString: typeof prvkey === 'string', + prvkeyLength: prvkey.length, + prvkeySample: prvkey.substring(0, 10) + '...', + encodedPrvkeyType: typeof new TextEncoder().encode(prvkey), + encodedPrvkeyLength: new TextEncoder().encode(prvkey).length, + eventTemplate }) - const result = await this.relayHub.publishEvent(giftWrap) + // Convert hex string to Uint8Array properly + const prvkeyBytes = this.hexToUint8Array(prvkey) + console.log('🔧 prvkeyBytes debug:', { + prvkeyBytesType: typeof prvkeyBytes, + prvkeyBytesLength: prvkeyBytes.length, + prvkeyBytesIsUint8Array: prvkeyBytes instanceof Uint8Array + }) + const event = finalizeEvent(eventTemplate, prvkeyBytes) + const result = await this.relayHub.publishEvent(event) + console.log('Order published to nostrmarket:', { orderId: order.id, - eventId: giftWrap.id, + eventId: result, merchantPubkey, - content: orderData + content: orderData, + encryptedContent: encryptedContent.substring(0, 50) + '...' }) return result.success.toString() diff --git a/src/modules/market/stores/market.ts b/src/modules/market/stores/market.ts index 7c0d8b1..b1a11ba 100644 --- a/src/modules/market/stores/market.ts +++ b/src/modules/market/stores/market.ts @@ -239,23 +239,14 @@ export const useMarketStore = defineStore('market', () => { } const addProduct = (product: Product) => { - // Lookup stallName from the current stall set — the value passed in by - // the caller can be stale ("Unknown Stall") if the stall event hadn't - // arrived yet. The reverse race (stall arrives first) is handled in - // addStall below. - const matchedStall = stalls.value.find(s => s.id === product.stall_id) - const enriched: Product = matchedStall - ? { ...product, stallName: matchedStall.name } - : product - - const existingIndex = products.value.findIndex(p => p.id === enriched.id) + const existingIndex = products.value.findIndex(p => p.id === product.id) if (existingIndex >= 0) { - products.value[existingIndex] = enriched + products.value[existingIndex] = product } else { - products.value.push(enriched) + products.value.push(product) } } - + const addStall = (stall: Stall) => { const existingIndex = stalls.value.findIndex(s => s.id === stall.id) if (existingIndex >= 0) { @@ -263,14 +254,6 @@ export const useMarketStore = defineStore('market', () => { } else { stalls.value.push(stall) } - // Re-stamp stallName on any products that arrived before this stall did - // (or whose stall name has changed). Direct property mutation on items - // in a reactive array triggers Vue's deep reactivity. - products.value.forEach(p => { - if (p.stall_id === stall.id && p.stallName !== stall.name) { - p.stallName = stall.name - } - }) } const addMarket = (market: Market) => { diff --git a/src/modules/market/views/CheckoutPage.vue b/src/modules/market/views/CheckoutPage.vue index 997ad39..9e87ab7 100644 --- a/src/modules/market/views/CheckoutPage.vue +++ b/src/modules/market/views/CheckoutPage.vue @@ -241,7 +241,6 @@
- -

+

{{ orderValidationMessage }}

-

- {{ t('market.auth.loginPrompt') }} -

@@ -275,9 +262,7 @@ diff --git a/src/modules/market/views/MarketPage.vue b/src/modules/market/views/MarketPage.vue index 2428577..b4ca8e2 100644 --- a/src/modules/market/views/MarketPage.vue +++ b/src/modules/market/views/MarketPage.vue @@ -67,6 +67,9 @@ @view-stall="viewStall" /> + + + @@ -83,6 +86,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar' import MarketSearchBar from '../components/MarketSearchBar.vue' import ProductGrid from '../components/ProductGrid.vue' import CategoryFilterBar from '../components/CategoryFilterBar.vue' +import CartButton from '../components/CartButton.vue' import LoadingErrorState from '../components/LoadingErrorState.vue' import type { Product } from '../types/market' import type { FuzzySearchOptions } from '@/composables/useFuzzySearch' diff --git a/src/modules/market/views/StallView.vue b/src/modules/market/views/StallView.vue index 5d64365..96322ad 100644 --- a/src/modules/market/views/StallView.vue +++ b/src/modules/market/views/StallView.vue @@ -131,6 +131,9 @@ /> + + + - - diff --git a/src/tasks-app/App.vue b/src/tasks-app/App.vue deleted file mode 100644 index 8c7f8ba..0000000 --- a/src/tasks-app/App.vue +++ /dev/null @@ -1,47 +0,0 @@ - - - diff --git a/src/tasks-app/app.config.ts b/src/tasks-app/app.config.ts deleted file mode 100644 index 246e7d9..0000000 --- a/src/tasks-app/app.config.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { AppConfig } from '@/core/types' - -/** - * Standalone Tasks app configuration. - * Only enables base + tasks modules. - */ -export const appConfig: AppConfig = { - modules: { - base: { - name: 'base', - enabled: true, - lazy: false, - config: { - nostr: { - relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '["wss://relay.damus.io", "wss://nos.lol"]') - }, - auth: { - sessionTimeout: 24 * 60 * 60 * 1000, - }, - pwa: { - autoPrompt: true - }, - imageUpload: { - baseUrl: import.meta.env.VITE_PICTRS_BASE_URL || 'https://img.mydomain.com', - maxSizeMB: 10, - acceptedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'] - } - } - }, - tasks: { - name: 'tasks', - enabled: true, - lazy: false, - config: { - maxTasks: 200, - adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]') - } - }, - }, - - features: { - pwa: true, - pushNotifications: true, - electronApp: false, - developmentMode: import.meta.env.DEV - } -} - -export default appConfig diff --git a/src/tasks-app/app.ts b/src/tasks-app/app.ts deleted file mode 100644 index 2223842..0000000 --- a/src/tasks-app/app.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { createApp } from 'vue' -import { createRouter, createWebHistory } from 'vue-router' -import { createPinia } from 'pinia' -import { pluginManager } from '@/core/plugin-manager' -import { eventBus } from '@/core/event-bus' -import { container } from '@/core/di-container' - -import appConfig from './app.config' -import baseModule from '@/modules/base' -import tasksModule from '@/modules/tasks' - -import App from './App.vue' - -import '@/assets/index.css' -import { i18n, changeLocale, type AvailableLocale } from '@/i18n' -import { installLenientAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers' -import { acceptTokenFromUrl } from '@/lib/url-token' - -export async function createAppInstance() { - console.log('Starting Tasks app...') - - acceptTokenFromUrl('Tasks') - - const app = createApp(App) - - const moduleRoutes = [ - ...baseModule.routes || [], - ...tasksModule.routes || [], - ].filter(Boolean) - - const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes: [ - { - path: '/', - redirect: '/tasks' - }, - { - path: '/login', - name: 'login', - component: import.meta.env.VITE_DEMO_MODE === 'true' - ? () => import('@/pages/LoginDemo.vue') - : () => import('@/pages/Login.vue'), - meta: { requiresAuth: false } - }, - ...moduleRoutes, - catchAllRoute, - ] - }) - - installLenientAuthGuard(router) - - const pinia = createPinia() - - app.use(router) - app.use(pinia) - app.use(i18n) - - const defaultLocale = import.meta.env.VITE_DEFAULT_LOCALE as AvailableLocale | undefined - if (defaultLocale && !localStorage.getItem('user-locale')) { - await changeLocale(defaultLocale) - } - - pluginManager.init(app, router) - - const moduleRegistrations = [] - - if (appConfig.modules.base.enabled) { - moduleRegistrations.push( - pluginManager.register(baseModule, appConfig.modules.base) - ) - } - - if (appConfig.modules.tasks?.enabled) { - moduleRegistrations.push( - pluginManager.register(tasksModule, appConfig.modules.tasks) - ) - } - - 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() - markAuthReady(auth) - - app.config.errorHandler = (err, _vm, info) => { - console.error('Global error:', err, info) - eventBus.emit('app:error', { error: err, info }, 'app') - } - - if (appConfig.features.developmentMode) { - ;(window as any).__pluginManager = pluginManager - ;(window as any).__eventBus = eventBus - ;(window as any).__container = container - } - - console.log('Tasks app initialized') - return { app, router } -} - -export async function startApp() { - try { - const { app } = await createAppInstance() - app.mount('#app') - console.log('Tasks app started!') - eventBus.emit('app:started', {}, 'app') - } catch (error) { - console.error('Failed to start Tasks app:', error) - document.getElementById('app')!.innerHTML = ` -
-

Failed to Start

-

${error instanceof Error ? error.message : 'Unknown error'}

-

Please refresh the page.

-
- ` - } -} diff --git a/src/tasks-app/main.ts b/src/tasks-app/main.ts deleted file mode 100644 index 25e84da..0000000 --- a/src/tasks-app/main.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { startApp } from './app' -import { registerSW } from 'virtual:pwa-register' -import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup' -import 'vue-sonner/style.css' - -cleanupStaleDevServiceWorkers() - -const intervalMS = 60 * 60 * 1000 -registerSW({ - onRegistered(r) { - r && setInterval(() => { - r.update() - }, intervalMS) - }, - onOfflineReady() { - console.log('Tasks app ready to work offline') - } -}) - -startApp() diff --git a/src/wallet-app/App.vue b/src/wallet-app/App.vue deleted file mode 100644 index d4e6045..0000000 --- a/src/wallet-app/App.vue +++ /dev/null @@ -1,48 +0,0 @@ - - - diff --git a/src/wallet-app/app.config.ts b/src/wallet-app/app.config.ts deleted file mode 100644 index a38c7ab..0000000 --- a/src/wallet-app/app.config.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { AppConfig } from '@/core/types' - -/** - * Standalone Wallet app configuration. - * Only enables base + wallet modules. - */ -export const appConfig: AppConfig = { - modules: { - base: { - name: 'base', - enabled: true, - lazy: false, - config: { - nostr: { - relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '["wss://relay.damus.io", "wss://nos.lol"]') - }, - auth: { - sessionTimeout: 24 * 60 * 60 * 1000, - }, - pwa: { - autoPrompt: true - }, - imageUpload: { - baseUrl: import.meta.env.VITE_PICTRS_BASE_URL || 'https://img.mydomain.com', - maxSizeMB: 10, - acceptedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif', 'image/gif'] - } - } - }, - wallet: { - name: 'wallet', - enabled: true, - lazy: false, - config: { - defaultReceiveAmount: 1000, - maxReceiveAmount: 1000000, - apiConfig: { - baseUrl: import.meta.env.VITE_LNBITS_BASE_URL || 'http://localhost:5000' - }, - websocket: { - enabled: import.meta.env.VITE_WEBSOCKET_ENABLED !== 'false', - reconnectDelay: 2000, - maxReconnectAttempts: 3, - fallbackToPolling: true, - pollingInterval: 10000 - } - } - }, - }, - - features: { - pwa: true, - pushNotifications: true, - electronApp: false, - developmentMode: import.meta.env.DEV - } -} - -export default appConfig diff --git a/src/wallet-app/app.ts b/src/wallet-app/app.ts deleted file mode 100644 index e9fc6ae..0000000 --- a/src/wallet-app/app.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { createApp } from 'vue' -import { createRouter, createWebHistory } from 'vue-router' -import { createPinia } from 'pinia' -import { pluginManager } from '@/core/plugin-manager' -import { eventBus } from '@/core/event-bus' -import { container } from '@/core/di-container' - -import appConfig from './app.config' -import baseModule from '@/modules/base' -import walletModule from '@/modules/wallet' - -import App from './App.vue' - -import '@/assets/index.css' -import { i18n, changeLocale, type AvailableLocale } from '@/i18n' -import { installStrictAuthGuard, markAuthReady, catchAllRoute } from '@/lib/router-helpers' -import { acceptTokenFromUrl } from '@/lib/url-token' - -export async function createAppInstance() { - console.log('Starting Wallet app...') - - acceptTokenFromUrl('Wallet') - - const app = createApp(App) - - const moduleRoutes = [ - ...baseModule.routes || [], - ...walletModule.routes || [], - ].filter(Boolean) - - const router = createRouter({ - history: createWebHistory(import.meta.env.BASE_URL), - routes: [ - { - path: '/', - redirect: '/wallet' - }, - { - path: '/login', - name: 'login', - component: import.meta.env.VITE_DEMO_MODE === 'true' - ? () => import('@/pages/LoginDemo.vue') - : () => import('@/pages/Login.vue'), - 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) - app.use(pinia) - app.use(i18n) - - const defaultLocale = import.meta.env.VITE_DEFAULT_LOCALE as AvailableLocale | undefined - if (defaultLocale && !localStorage.getItem('user-locale')) { - await changeLocale(defaultLocale) - } - - pluginManager.init(app, router) - - const moduleRegistrations = [] - - if (appConfig.modules.base.enabled) { - moduleRegistrations.push( - pluginManager.register(baseModule, appConfig.modules.base) - ) - } - - if (appConfig.modules.wallet?.enabled) { - moduleRegistrations.push( - pluginManager.register(walletModule, appConfig.modules.wallet) - ) - } - - 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() - markAuthReady(auth) - - app.config.errorHandler = (err, _vm, info) => { - console.error('Global error:', err, info) - eventBus.emit('app:error', { error: err, info }, 'app') - } - - if (appConfig.features.developmentMode) { - ;(window as any).__pluginManager = pluginManager - ;(window as any).__eventBus = eventBus - ;(window as any).__container = container - } - - console.log('Wallet app initialized') - return { app, router } -} - -export async function startApp() { - try { - const { app } = await createAppInstance() - app.mount('#app') - console.log('Wallet app started!') - eventBus.emit('app:started', {}, 'app') - } catch (error) { - console.error('Failed to start Wallet app:', error) - document.getElementById('app')!.innerHTML = ` -
-

Failed to Start

-

${error instanceof Error ? error.message : 'Unknown error'}

-

Please refresh the page.

-
- ` - } -} diff --git a/src/wallet-app/main.ts b/src/wallet-app/main.ts deleted file mode 100644 index 2039d3f..0000000 --- a/src/wallet-app/main.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { startApp } from './app' -import { registerSW } from 'virtual:pwa-register' -import { cleanupStaleDevServiceWorkers } from '@/lib/dev-sw-cleanup' -import 'vue-sonner/style.css' - -cleanupStaleDevServiceWorkers() - -const intervalMS = 60 * 60 * 1000 -registerSW({ - onRegistered(r) { - r && setInterval(() => { - r.update() - }, intervalMS) - }, - onOfflineReady() { - console.log('Wallet app ready to work offline') - } -}) - -startApp() diff --git a/tasks.html b/tasks.html deleted file mode 100644 index 6a5b49a..0000000 --- a/tasks.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - Tasks — Work Orders - - - - -
- - - diff --git a/vite.activities.config.ts b/vite.activities.config.ts index b7627e4..59ad437 100644 --- a/vite.activities.config.ts +++ b/vite.activities.config.ts @@ -15,16 +15,13 @@ function activitiesHtmlPlugin(): Plugin { name: 'activities-html-rewrite', configureServer(server) { server.middlewares.use((req, _res, next) => { - // Rewrite all non-asset requests to activities.html. - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' + // Rewrite all non-asset requests to activities.html if ( req.url && !req.url.startsWith('/@') && !req.url.startsWith('/src/') && !req.url.startsWith('/node_modules/') && - !path.includes('.') + !req.url.includes('.') // skip files with extensions ) { req.url = '/activities.html' } @@ -43,12 +40,6 @@ function activitiesHtmlPlugin(): Plugin { */ export default defineConfig(({ mode }) => ({ base: process.env.VITE_BASE_PATH || '/', - // Per-app dep cache so concurrent dev servers don't race on .vite/deps - cacheDir: 'node_modules/.vite-activities', - server: { - port: 5181, - strictPort: true, - }, plugins: [ activitiesHtmlPlugin(), vue(), @@ -56,7 +47,7 @@ export default defineConfig(({ mode }) => ({ VitePWA({ registerType: 'autoUpdate', devOptions: { - enabled: false, + enabled: true, }, workbox: { globPatterns: ['**/*.{js,css,html,ico,png,svg}'], diff --git a/vite.castle.config.ts b/vite.castle.config.ts index 6ec7e0d..c16c2a2 100644 --- a/vite.castle.config.ts +++ b/vite.castle.config.ts @@ -15,16 +15,13 @@ function castleHtmlPlugin(): Plugin { name: 'castle-html-rewrite', configureServer(server) { server.middlewares.use((req, _res, next) => { - // Rewrite all non-asset requests to castle.html. - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' + // Rewrite all non-asset requests to castle.html if ( req.url && !req.url.startsWith('/@') && !req.url.startsWith('/src/') && !req.url.startsWith('/node_modules/') && - !path.includes('.') + !req.url.includes('.') // skip files with extensions ) { req.url = '/castle.html' } @@ -43,12 +40,6 @@ function castleHtmlPlugin(): Plugin { */ export default defineConfig(({ mode }) => ({ base: process.env.VITE_BASE_PATH || '/', - // Per-app dep cache so concurrent dev servers don't race on .vite/deps - cacheDir: 'node_modules/.vite-castle', - server: { - port: 5180, - strictPort: true, - }, plugins: [ castleHtmlPlugin(), vue(), @@ -56,7 +47,7 @@ export default defineConfig(({ mode }) => ({ VitePWA({ registerType: 'autoUpdate', devOptions: { - enabled: false, + enabled: true, }, workbox: { globPatterns: ['**/*.{js,css,html,ico,png,svg}'], @@ -110,13 +101,10 @@ export default defineConfig(({ mode }) => ({ ], resolve: { alias: { - // ORDER MATTERS — @rollup/plugin-alias is first-match-wins. - // The more specific @/app.config remap must precede the @ prefix - // alias, otherwise '@/app.config' matches '@' first and resolves - // to ./src/app.config (the hub config). ExpensesAPI etc. import - // from @/app.config and need the per-app config. - '@/app.config': fileURLToPath(new URL('./src/accounting-app/app.config.ts', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)), + // CRITICAL: Remap @/app.config to the castle app's config + // ExpensesAPI and other modules import from @/app.config directly + '@/app.config': fileURLToPath(new URL('./src/accounting-app/app.config.ts', import.meta.url)), }, }, build: { diff --git a/vite.chat.config.ts b/vite.chat.config.ts deleted file mode 100644 index c28535f..0000000 --- a/vite.chat.config.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { fileURLToPath, URL } from 'node:url' -import vue from '@vitejs/plugin-vue' -import tailwindcss from '@tailwindcss/vite' -import { defineConfig, type Plugin } from 'vite' -import { VitePWA } from 'vite-plugin-pwa' -import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' -import { visualizer } from 'rollup-plugin-visualizer' - -function chatHtmlPlugin(): Plugin { - return { - name: 'chat-html-rewrite', - configureServer(server) { - server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' - if ( - req.url && - !req.url.startsWith('/@') && - !req.url.startsWith('/src/') && - !req.url.startsWith('/node_modules/') && - !path.includes('.') - ) { - req.url = '/chat.html' - } - next() - }) - }, - } -} - -/** - * Vite config for the standalone Chat app. - * - * Set VITE_BASE_PATH to deploy under a path prefix: - * VITE_BASE_PATH=/chat/ → app.${domain}/chat/ (shared auth) - * (default: /) → chat.${domain} (standalone subdomain) - */ -export default defineConfig(({ mode }) => ({ - base: process.env.VITE_BASE_PATH || '/', - // Per-app dep cache so concurrent dev servers don't race on .vite/deps - cacheDir: 'node_modules/.vite-chat', - server: { - port: 5183, - strictPort: true, - }, - plugins: [ - chatHtmlPlugin(), - vue(), - tailwindcss(), - VitePWA({ - registerType: 'autoUpdate', - devOptions: { enabled: false }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg}'], - navigateFallback: 'chat.html', - navigateFallbackAllowlist: [ - new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), - ], - }, - includeAssets: [ - 'favicon.ico', - 'apple-touch-icon.png', - 'mask-icon.svg', - 'icon-192.png', - 'icon-512.png', - 'icon-maskable-192.png', - 'icon-maskable-512.png', - ], - manifest: { - name: 'Chat — Encrypted', - short_name: 'Chat', - description: 'End-to-end encrypted Nostr chat', - theme_color: '#16a34a', - background_color: '#ffffff', - display: 'standalone', - orientation: 'portrait-primary', - start_url: process.env.VITE_BASE_PATH || '/', - scope: process.env.VITE_BASE_PATH || '/', - id: 'chat-app', - categories: ['social', 'communication'], - lang: 'en', - icons: [ - { src: 'icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, - { src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, - { src: 'icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' }, - { src: 'icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, - ], - }, - }), - ViteImageOptimizer({ - jpg: { quality: 80 }, - png: { quality: 80 }, - webp: { lossless: true }, - }), - mode === 'analyze' && - visualizer({ - open: true, - filename: 'dist-chat/stats.html', - gzipSize: true, - brotliSize: true, - }), - ], - resolve: { - alias: { - // ORDER MATTERS — @/app.config must precede @ (first-match-wins). - '@/app.config': fileURLToPath(new URL('./src/chat-app/app.config.ts', import.meta.url)), - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - build: { - outDir: 'dist-chat', - rollupOptions: { - input: 'chat.html', - output: { - manualChunks: { - 'vue-vendor': ['vue', 'vue-router', 'pinia'], - 'ui-vendor': ['radix-vue', '@vueuse/core'], - 'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge'], - }, - }, - }, - chunkSizeWarningLimit: 1000, - }, -})) diff --git a/vite.config.ts b/vite.config.ts index fd07bc1..fee1c90 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -9,21 +9,13 @@ import { visualizer } from 'rollup-plugin-visualizer' // https://vite.dev/config/ export default defineConfig(({ mode }) => ({ - // Per-app dep cache so concurrent dev servers don't race on .vite/deps - cacheDir: 'node_modules/.vite-hub', - server: { - port: 5173, - strictPort: true, - }, plugins: [ vue(), tailwindcss(), VitePWA({ registerType: 'autoUpdate', devOptions: { - // SW disabled in dev — was caching stale bundles across restarts. - // Run `npm run preview` to test PWA behaviour against a real build. - enabled: false + enabled: true }, // strategies: 'injectManifest', srcDir: 'public', @@ -33,7 +25,7 @@ export default defineConfig(({ mode }) => ({ '**/*.{js,css,html,ico,png,svg}' ], // Don't intercept standalone app paths — they have their own service workers - navigateFallbackDenylist: [/^\/sortir\//, /^\/castle\//, /^\/wallet\//, /^\/chat\//, /^\/market\//, /^\/cart\//, /^\/checkout\//, /^\/tasks\//, /^\/forum\//, /^\/submit\//, /^\/submission\//], + navigateFallbackDenylist: [/^\/sortir\//, /^\/castle\//], }, includeAssets: [ 'favicon.ico', diff --git a/vite.forum.config.ts b/vite.forum.config.ts deleted file mode 100644 index 0fdebbe..0000000 --- a/vite.forum.config.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { fileURLToPath, URL } from 'node:url' -import vue from '@vitejs/plugin-vue' -import tailwindcss from '@tailwindcss/vite' -import { defineConfig, type Plugin } from 'vite' -import { VitePWA } from 'vite-plugin-pwa' -import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' -import { visualizer } from 'rollup-plugin-visualizer' - -function forumHtmlPlugin(): Plugin { - return { - name: 'forum-html-rewrite', - configureServer(server) { - server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' - if ( - req.url && - !req.url.startsWith('/@') && - !req.url.startsWith('/src/') && - !req.url.startsWith('/node_modules/') && - !path.includes('.') - ) { - req.url = '/forum.html' - } - next() - }) - }, - } -} - -/** - * Vite config for the standalone Forum app. - * - * Set VITE_BASE_PATH to deploy under a path prefix: - * VITE_BASE_PATH=/forum/ → app.${domain}/forum/ (shared auth) - * (default: /) → forum.${domain} (standalone subdomain) - */ -export default defineConfig(({ mode }) => ({ - base: process.env.VITE_BASE_PATH || '/', - // Per-app dep cache so concurrent dev servers don't race on .vite/deps - cacheDir: 'node_modules/.vite-forum', - server: { - port: 5184, - strictPort: true, - }, - plugins: [ - forumHtmlPlugin(), - vue(), - tailwindcss(), - VitePWA({ - registerType: 'autoUpdate', - devOptions: { enabled: false }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg}'], - navigateFallback: 'forum.html', - navigateFallbackAllowlist: [ - new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), - ], - }, - includeAssets: [ - 'favicon.ico', - 'apple-touch-icon.png', - 'mask-icon.svg', - 'icon-192.png', - 'icon-512.png', - 'icon-maskable-192.png', - 'icon-maskable-512.png', - ], - manifest: { - name: 'Forum — Discussions', - short_name: 'Forum', - description: 'Decentralized link aggregator and discussion forum on Nostr', - theme_color: '#2563eb', - background_color: '#ffffff', - display: 'standalone', - orientation: 'portrait-primary', - start_url: process.env.VITE_BASE_PATH || '/', - scope: process.env.VITE_BASE_PATH || '/', - id: 'forum-app', - categories: ['social', 'news'], - lang: 'en', - icons: [ - { src: 'icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, - { src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, - { src: 'icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' }, - { src: 'icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, - ], - }, - }), - ViteImageOptimizer({ - jpg: { quality: 80 }, - png: { quality: 80 }, - webp: { lossless: true }, - }), - mode === 'analyze' && - visualizer({ - open: true, - filename: 'dist-forum/stats.html', - gzipSize: true, - brotliSize: true, - }), - ], - resolve: { - alias: { - // ORDER MATTERS — @/app.config must precede @ (first-match-wins). - '@/app.config': fileURLToPath(new URL('./src/forum-app/app.config.ts', import.meta.url)), - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - build: { - outDir: 'dist-forum', - rollupOptions: { - input: 'forum.html', - output: { - manualChunks: { - 'vue-vendor': ['vue', 'vue-router', 'pinia'], - 'ui-vendor': ['radix-vue', '@vueuse/core'], - 'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge'], - }, - }, - }, - chunkSizeWarningLimit: 1000, - }, -})) diff --git a/vite.market.config.ts b/vite.market.config.ts deleted file mode 100644 index bf38430..0000000 --- a/vite.market.config.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { fileURLToPath, URL } from 'node:url' -import vue from '@vitejs/plugin-vue' -import tailwindcss from '@tailwindcss/vite' -import { defineConfig, type Plugin } from 'vite' -import { VitePWA } from 'vite-plugin-pwa' -import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' -import { visualizer } from 'rollup-plugin-visualizer' - -function marketHtmlPlugin(): Plugin { - return { - name: 'market-html-rewrite', - configureServer(server) { - server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' - if ( - req.url && - !req.url.startsWith('/@') && - !req.url.startsWith('/src/') && - !req.url.startsWith('/node_modules/') && - !path.includes('.') - ) { - req.url = '/market.html' - } - next() - }) - }, - } -} - -/** - * Vite config for the standalone Market app. - * - * Set VITE_BASE_PATH to deploy under a path prefix: - * VITE_BASE_PATH=/market/ → app.${domain}/market/ (shared auth) - * (default: /) → market.${domain} (standalone subdomain) - */ -export default defineConfig(({ mode }) => ({ - base: process.env.VITE_BASE_PATH || '/', - // Per-app dep cache so concurrent dev servers don't race on .vite/deps - cacheDir: 'node_modules/.vite-market', - server: { - port: 5185, - strictPort: true, - }, - plugins: [ - marketHtmlPlugin(), - vue(), - tailwindcss(), - VitePWA({ - registerType: 'autoUpdate', - devOptions: { enabled: false }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg}'], - navigateFallback: 'market.html', - navigateFallbackAllowlist: [ - new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), - ], - }, - includeAssets: [ - 'favicon.ico', - 'apple-touch-icon.png', - 'mask-icon.svg', - 'icon-192.png', - 'icon-512.png', - 'icon-maskable-192.png', - 'icon-maskable-512.png', - ], - manifest: { - name: 'Market — Nostr', - short_name: 'Market', - description: 'Decentralized marketplace on Nostr with Lightning payments', - theme_color: '#dc2626', - background_color: '#ffffff', - display: 'standalone', - orientation: 'portrait-primary', - start_url: process.env.VITE_BASE_PATH || '/', - scope: process.env.VITE_BASE_PATH || '/', - id: 'market-app', - categories: ['shopping', 'business'], - lang: 'en', - icons: [ - { src: 'icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, - { src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, - { src: 'icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' }, - { src: 'icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, - ], - }, - }), - ViteImageOptimizer({ - jpg: { quality: 80 }, - png: { quality: 80 }, - webp: { lossless: true }, - }), - mode === 'analyze' && - visualizer({ - open: true, - filename: 'dist-market/stats.html', - gzipSize: true, - brotliSize: true, - }), - ], - resolve: { - alias: { - // ORDER MATTERS — @/app.config must precede @ (first-match-wins). - '@/app.config': fileURLToPath(new URL('./src/market-app/app.config.ts', import.meta.url)), - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - build: { - outDir: 'dist-market', - rollupOptions: { - input: 'market.html', - output: { - manualChunks: { - 'vue-vendor': ['vue', 'vue-router', 'pinia'], - 'ui-vendor': ['radix-vue', '@vueuse/core'], - 'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge'], - }, - }, - }, - chunkSizeWarningLimit: 1000, - }, -})) diff --git a/vite.tasks.config.ts b/vite.tasks.config.ts deleted file mode 100644 index 1edc3e6..0000000 --- a/vite.tasks.config.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { fileURLToPath, URL } from 'node:url' -import vue from '@vitejs/plugin-vue' -import tailwindcss from '@tailwindcss/vite' -import { defineConfig, type Plugin } from 'vite' -import { VitePWA } from 'vite-plugin-pwa' -import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' -import { visualizer } from 'rollup-plugin-visualizer' - -function tasksHtmlPlugin(): Plugin { - return { - name: 'tasks-html-rewrite', - configureServer(server) { - server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' - if ( - req.url && - !req.url.startsWith('/@') && - !req.url.startsWith('/src/') && - !req.url.startsWith('/node_modules/') && - !path.includes('.') - ) { - req.url = '/tasks.html' - } - next() - }) - }, - } -} - -/** - * Vite config for the standalone Tasks app. - * - * Set VITE_BASE_PATH to deploy under a path prefix: - * VITE_BASE_PATH=/tasks/ → app.${domain}/tasks/ (shared auth) - * (default: /) → tasks.${domain} (standalone subdomain) - */ -export default defineConfig(({ mode }) => ({ - base: process.env.VITE_BASE_PATH || '/', - // Per-app dep cache so concurrent dev servers don't race on .vite/deps - cacheDir: 'node_modules/.vite-tasks', - server: { - port: 5186, - strictPort: true, - }, - plugins: [ - tasksHtmlPlugin(), - vue(), - tailwindcss(), - VitePWA({ - registerType: 'autoUpdate', - devOptions: { enabled: false }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg}'], - navigateFallback: 'tasks.html', - navigateFallbackAllowlist: [ - new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), - ], - }, - includeAssets: [ - 'favicon.ico', - 'apple-touch-icon.png', - 'mask-icon.svg', - 'icon-192.png', - 'icon-512.png', - 'icon-maskable-192.png', - 'icon-maskable-512.png', - ], - manifest: { - name: 'Tasks — Work Orders', - short_name: 'Tasks', - description: 'Decentralized task management on Nostr', - theme_color: '#4338ca', - background_color: '#ffffff', - display: 'standalone', - orientation: 'portrait-primary', - start_url: process.env.VITE_BASE_PATH || '/', - scope: process.env.VITE_BASE_PATH || '/', - id: 'tasks-app', - categories: ['productivity', 'business'], - lang: 'en', - icons: [ - { src: 'icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, - { src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, - { src: 'icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' }, - { src: 'icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, - ], - }, - }), - ViteImageOptimizer({ - jpg: { quality: 80 }, - png: { quality: 80 }, - webp: { lossless: true }, - }), - mode === 'analyze' && - visualizer({ - open: true, - filename: 'dist-tasks/stats.html', - gzipSize: true, - brotliSize: true, - }), - ], - resolve: { - alias: { - // ORDER MATTERS — @/app.config must precede @ (first-match-wins). - '@/app.config': fileURLToPath(new URL('./src/tasks-app/app.config.ts', import.meta.url)), - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - build: { - outDir: 'dist-tasks', - rollupOptions: { - input: 'tasks.html', - output: { - manualChunks: { - 'vue-vendor': ['vue', 'vue-router', 'pinia'], - 'ui-vendor': ['radix-vue', '@vueuse/core'], - 'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge'], - }, - }, - }, - chunkSizeWarningLimit: 1000, - }, -})) diff --git a/vite.wallet.config.ts b/vite.wallet.config.ts deleted file mode 100644 index f991672..0000000 --- a/vite.wallet.config.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { fileURLToPath, URL } from 'node:url' -import vue from '@vitejs/plugin-vue' -import tailwindcss from '@tailwindcss/vite' -import { defineConfig, type Plugin } from 'vite' -import { VitePWA } from 'vite-plugin-pwa' -import { ViteImageOptimizer } from 'vite-plugin-image-optimizer' -import { visualizer } from 'rollup-plugin-visualizer' - -/** - * Plugin to rewrite dev server requests to wallet.html - * (SPA fallback for the standalone Wallet app entry point) - */ -function walletHtmlPlugin(): Plugin { - return { - name: 'wallet-html-rewrite', - configureServer(server) { - server.middlewares.use((req, _res, next) => { - // Strip query before checking for an extension — JWTs (e.g. ?token=...) - // contain dots and would otherwise get mistaken for an asset request. - const path = req.url ? req.url.split('?')[0] : '' - if ( - req.url && - !req.url.startsWith('/@') && - !req.url.startsWith('/src/') && - !req.url.startsWith('/node_modules/') && - !path.includes('.') - ) { - req.url = '/wallet.html' - } - next() - }) - }, - } -} - -/** - * Vite config for the standalone Wallet app. - * - * Set VITE_BASE_PATH to deploy under a path prefix: - * VITE_BASE_PATH=/wallet/ → app.${domain}/wallet/ (shared auth) - * (default: /) → wallet.${domain} (standalone subdomain) - */ -export default defineConfig(({ mode }) => ({ - base: process.env.VITE_BASE_PATH || '/', - // Per-app dep cache so concurrent dev servers don't race on .vite/deps - cacheDir: 'node_modules/.vite-wallet', - server: { - port: 5182, - strictPort: true, - }, - plugins: [ - walletHtmlPlugin(), - vue(), - tailwindcss(), - VitePWA({ - registerType: 'autoUpdate', - devOptions: { - enabled: false, - }, - workbox: { - globPatterns: ['**/*.{js,css,html,ico,png,svg}'], - navigateFallback: 'wallet.html', - navigateFallbackAllowlist: [ - new RegExp(`^${(process.env.VITE_BASE_PATH || '/').replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`), - ], - }, - includeAssets: [ - 'favicon.ico', - 'apple-touch-icon.png', - 'mask-icon.svg', - 'icon-192.png', - 'icon-512.png', - 'icon-maskable-192.png', - 'icon-maskable-512.png', - ], - manifest: { - name: 'Wallet — Lightning', - short_name: 'Wallet', - description: 'Lightning Network wallet — send, receive, and manage sats', - theme_color: '#eab308', - background_color: '#ffffff', - display: 'standalone', - orientation: 'portrait-primary', - start_url: process.env.VITE_BASE_PATH || '/', - scope: process.env.VITE_BASE_PATH || '/', - id: 'wallet-app', - categories: ['finance'], - lang: 'en', - icons: [ - { src: 'icon-192.png', sizes: '192x192', type: 'image/png', purpose: 'any' }, - { src: 'icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, - { src: 'icon-maskable-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' }, - { src: 'icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, - ], - }, - }), - ViteImageOptimizer({ - jpg: { quality: 80 }, - png: { quality: 80 }, - webp: { lossless: true }, - }), - mode === 'analyze' && - visualizer({ - open: true, - filename: 'dist-wallet/stats.html', - gzipSize: true, - brotliSize: true, - }), - ], - resolve: { - alias: { - // ORDER MATTERS — @/app.config must precede @ (first-match-wins). - '@/app.config': fileURLToPath(new URL('./src/wallet-app/app.config.ts', import.meta.url)), - '@': fileURLToPath(new URL('./src', import.meta.url)), - }, - }, - build: { - outDir: 'dist-wallet', - rollupOptions: { - input: 'wallet.html', - output: { - manualChunks: { - 'vue-vendor': ['vue', 'vue-router', 'pinia'], - 'ui-vendor': ['radix-vue', '@vueuse/core'], - 'shadcn': ['class-variance-authority', 'clsx', 'tailwind-merge'], - }, - }, - }, - chunkSizeWarningLimit: 1000, - }, -})) diff --git a/wallet.html b/wallet.html deleted file mode 100644 index 249f51d..0000000 --- a/wallet.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - Wallet — Lightning - - - - -
- - -