From 4feb5459cca66799b29de7e11b1636ccccfc7549 Mon Sep 17 00:00:00 2001 From: padreug Date: Sun, 7 Sep 2025 00:47:02 +0200 Subject: [PATCH] Refactor authentication architecture to eliminate dual auth complexity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This major refactor consolidates the authentication system to use a single source of truth, eliminating timing issues and architectural complexity that was causing chat and payment functionality problems. Key Changes: • Remove old global useAuth composable and replace with useAuthService wrapper • Update all 25+ files to use consistent auth pattern via dependency injection • Eliminate dual auth detection workarounds from services (ChatService, PaymentService, etc.) • Fix TypeScript errors and add proper Uint8Array conversion for Nostr private keys • Consolidate auth state management to AuthService as single source of truth Benefits: • Resolves chat peer loading and message subscription timing issues • Fixes wallet detection problems for Lightning payments • Eliminates race conditions between global and injected auth • Maintains API compatibility while improving architecture • Reduces code complexity and improves maintainability 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/App.vue | 2 +- src/app.ts | 2 +- src/components/auth/LoginDialog.vue | 2 +- src/components/auth/ProfileDialog.vue | 2 +- src/components/auth/UserProfile.vue | 2 +- src/components/demo/DemoAccountCreator.vue | 2 +- src/components/layout/Navbar.vue | 2 +- src/composables/useAuth.ts | 168 ------------ src/composables/useAuthService.ts | 72 ++++++ src/core/services/PaymentService.ts | 91 +------ src/modules/base/auth/auth-service.ts | 55 +++- src/modules/chat/services/chat-service.ts | 244 ++++-------------- .../components/PurchaseTicketDialog.vue | 14 +- .../events/composables/useTicketPurchase.ts | 38 +-- .../events/composables/useUserTickets.ts | 2 +- src/modules/events/views/EventsPage.vue | 2 +- src/modules/events/views/MyTicketsPage.vue | 2 +- .../market/components/DashboardOverview.vue | 2 +- .../market/components/MerchantStore.vue | 2 +- .../market/components/OrderHistory.vue | 2 +- .../components/PaymentRequestDialog.vue | 8 +- src/modules/market/composables/useMarket.ts | 2 +- .../composables/usePaymentStatusChecker.ts | 2 +- .../market/services/nostrmarketService.ts | 2 +- src/modules/market/stores/market.ts | 2 +- src/modules/market/views/CheckoutPage.vue | 2 +- src/pages/LoginDemo.vue | 2 +- 27 files changed, 210 insertions(+), 518 deletions(-) delete mode 100644 src/composables/useAuth.ts create mode 100644 src/composables/useAuthService.ts diff --git a/src/App.vue b/src/App.vue index f8e831b..59e840b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,7 +7,7 @@ import LoginDialog from '@/components/auth/LoginDialog.vue' import { Toaster } from '@/components/ui/sonner' import 'vue-sonner/style.css' import { useMarketPreloader } from '@/modules/market/composables/useMarketPreloader' -import { auth } from '@/composables/useAuth' +import { auth } from '@/composables/useAuthService' import { toast } from 'vue-sonner' const route = useRoute() diff --git a/src/app.ts b/src/app.ts index 8e0565b..7c3a7f9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -124,7 +124,7 @@ export async function createAppInstance() { await pluginManager.installAll() // Initialize auth before setting up router guards - const { auth } = await import('@/composables/useAuth') + const { auth } = await import('@/composables/useAuthService') await auth.initialize() console.log('Auth initialized, isAuthenticated:', auth.isAuthenticated.value) diff --git a/src/components/auth/LoginDialog.vue b/src/components/auth/LoginDialog.vue index 5e9ade4..fd84de9 100644 --- a/src/components/auth/LoginDialog.vue +++ b/src/components/auth/LoginDialog.vue @@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { User } from 'lucide-vue-next' -import { auth } from '@/composables/useAuth' +import { auth } from '@/composables/useAuthService' import { useToast } from '@/core/composables/useToast' interface Props { diff --git a/src/components/auth/ProfileDialog.vue b/src/components/auth/ProfileDialog.vue index 602bb71..da30ad1 100644 --- a/src/components/auth/ProfileDialog.vue +++ b/src/components/auth/ProfileDialog.vue @@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge' import { LogoutConfirmDialog } from '@/components/ui/LogoutConfirmDialog' import { User, LogOut, Settings, Key, Wallet, ExternalLink } from 'lucide-vue-next' -import { auth } from '@/composables/useAuth' +import { auth } from '@/composables/useAuthService' import { toast } from 'vue-sonner' interface Props { diff --git a/src/components/auth/UserProfile.vue b/src/components/auth/UserProfile.vue index 54ab4dc..f026407 100644 --- a/src/components/auth/UserProfile.vue +++ b/src/components/auth/UserProfile.vue @@ -6,7 +6,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Badge } from '@/components/ui/badge' import { LogoutConfirmDialog } from '@/components/ui/LogoutConfirmDialog' import { User, LogOut, Settings } from 'lucide-vue-next' -import { auth } from '@/composables/useAuth' +import { auth } from '@/composables/useAuthService' import { toast } from 'vue-sonner' const router = useRouter() diff --git a/src/components/demo/DemoAccountCreator.vue b/src/components/demo/DemoAccountCreator.vue index 77d3de1..b37ac61 100644 --- a/src/components/demo/DemoAccountCreator.vue +++ b/src/components/demo/DemoAccountCreator.vue @@ -85,7 +85,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { auth } from '@/composables/useAuth' +import { auth } from '@/composables/useAuthService' import { useDemoAccountGenerator } from '@/composables/useDemoAccountGenerator' import { toast } from 'vue-sonner' diff --git a/src/components/layout/Navbar.vue b/src/components/layout/Navbar.vue index 023fff2..e3a6321 100644 --- a/src/components/layout/Navbar.vue +++ b/src/components/layout/Navbar.vue @@ -12,7 +12,7 @@ import LoginDialog from '@/components/auth/LoginDialog.vue' import ProfileDialog from '@/components/auth/ProfileDialog.vue' import CurrencyDisplay from '@/components/ui/CurrencyDisplay.vue' import { LogoutConfirmDialog } from '@/components/ui/LogoutConfirmDialog' -import { auth } from '@/composables/useAuth' +import { auth } from '@/composables/useAuthService' import { useMarketPreloader } from '@/modules/market/composables/useMarketPreloader' import { useMarketStore } from '@/stores/market' import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container' diff --git a/src/composables/useAuth.ts b/src/composables/useAuth.ts deleted file mode 100644 index 7728e51..0000000 --- a/src/composables/useAuth.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { ref, computed } from 'vue' -import { lnbitsAPI, type User, type LoginCredentials, type RegisterData } from '@/lib/api/lnbits' -import { useMultiAsyncOperation } from '@/core/composables/useAsyncOperation' - -const currentUser = ref(null) - -// Shared async operations for auth actions -const authOperations = useMultiAsyncOperation<{ - initialize: User | null - login: User - register: User - logout: void -}>() - -export function useAuth() { - const isAuthenticated = computed(() => !!currentUser.value) - - // Get operation instances - const initializeOp = authOperations.createOperation('initialize') - const loginOp = authOperations.createOperation('login') - const registerOp = authOperations.createOperation('register') - const logoutOp = authOperations.createOperation('logout') - - /** - * Initialize authentication on app start - */ - async function initialize(): Promise { - try { - await initializeOp.execute(async () => { - if (lnbitsAPI.isAuthenticated()) { - const user = await lnbitsAPI.getCurrentUser() - currentUser.value = user - return user - } - return null - }, { - errorMessage: 'Failed to initialize authentication', - showToast: false // Don't show toast for initialization errors - }) - } catch { - // Clear invalid token on error - await logout() - } - } - - /** - * Login with username and password - */ - async function login(credentials: LoginCredentials): Promise { - await loginOp.execute(async () => { - await lnbitsAPI.login(credentials) - - // Get user details - const user = await lnbitsAPI.getCurrentUser() - currentUser.value = user - return user - }, { - errorMessage: 'Login failed' - }) - } - - /** - * Register new user - */ - async function register(data: RegisterData): Promise { - await registerOp.execute(async () => { - await lnbitsAPI.register(data) - - // Get user details - const user = await lnbitsAPI.getCurrentUser() - currentUser.value = user - return user - }, { - errorMessage: 'Registration failed' - }) - } - - /** - * Logout and clear user data - */ - async function logout(): Promise { - await logoutOp.execute(async () => { - // Clear local state - lnbitsAPI.logout() - currentUser.value = null - // Clear all auth operation states - authOperations.clearAll() - }, { - showToast: false // Don't show toast for logout - }) - } - - /** - * Update user password - */ - async function updatePassword(currentPassword: string, newPassword: string): Promise { - const updatePasswordOp = authOperations.createOperation('updatePassword' as any) - - return await updatePasswordOp.execute(async () => { - const updatedUser = await lnbitsAPI.updatePassword(currentPassword, newPassword) - currentUser.value = updatedUser - return updatedUser - }, { - errorMessage: 'Failed to update password' - }) - } - - /** - * Update user profile - */ - async function updateProfile(data: Partial): Promise { - const updateProfileOp = authOperations.createOperation('updateProfile' as any) - - return await updateProfileOp.execute(async () => { - const updatedUser = await lnbitsAPI.updateProfile(data) - currentUser.value = updatedUser - return updatedUser - }, { - errorMessage: 'Failed to update profile' - }) - } - - /** - * Check if user is authenticated - */ - function checkAuth(): boolean { - return lnbitsAPI.isAuthenticated() - } - - /** - * Get user display info - */ - const userDisplay = computed(() => { - if (!currentUser.value) return null - - return { - name: currentUser.value.username || currentUser.value.email || 'Anonymous', - username: currentUser.value.username, - email: currentUser.value.email, - id: currentUser.value.id, - shortId: currentUser.value.id.slice(0, 8) + '...' + currentUser.value.id.slice(-8) - } - }) - - - - return { - // State - currentUser: computed(() => currentUser.value), - isAuthenticated, - isLoading: computed(() => authOperations.isAnyLoading()), - error: computed(() => authOperations.hasAnyError() ? - (initializeOp.error.value || loginOp.error.value || registerOp.error.value || logoutOp.error.value) : null), - userDisplay, - - // Actions - initialize, - login, - register, - logout, - updatePassword, - updateProfile, - checkAuth - } -} - -// Export singleton instance for global state -export const auth = useAuth() \ No newline at end of file diff --git a/src/composables/useAuthService.ts b/src/composables/useAuthService.ts new file mode 100644 index 0000000..44850b8 --- /dev/null +++ b/src/composables/useAuthService.ts @@ -0,0 +1,72 @@ +import { injectService, SERVICE_TOKENS } from '@/core/di-container' +import type { AuthService } from '@/modules/base/auth/auth-service' + +/** + * Composable to access the injected auth service + * This replaces the global auth composable with the injected service + */ +export function useAuth() { + const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as AuthService + + if (!authService) { + throw new Error('AuthService not available. Make sure base module is installed.') + } + + return { + // State + currentUser: authService.currentUser, + isAuthenticated: authService.isAuthenticated, + isLoading: authService.isLoading, + error: authService.error, + userDisplay: authService.userDisplay, + + // Compatibility aliases + user: authService.user, + + // Actions + initialize: () => authService.initialize(), + login: (credentials: any) => authService.login(credentials), + register: (data: any) => authService.register(data), + logout: () => authService.logout(), + updatePassword: (current: string, newPass: string) => authService.updatePassword(current, newPass), + updateProfile: (data: any) => authService.updateProfile(data), + checkAuth: () => authService.checkAuth(), + refresh: () => authService.refresh() + } +} + +// Export singleton reference for compatibility +export function getAuthService() { + return injectService(SERVICE_TOKENS.AUTH_SERVICE) as AuthService +} + +// For files that import { auth } directly +export const auth = { + get currentUser() { + const service = getAuthService() + return service?.currentUser + }, + get isAuthenticated() { + const service = getAuthService() + return service?.isAuthenticated + }, + get isLoading() { + const service = getAuthService() + return service?.isLoading + }, + get error() { + const service = getAuthService() + return service?.error + }, + get userDisplay() { + const service = getAuthService() + return service?.userDisplay + }, + initialize: () => getAuthService()?.initialize(), + login: (credentials: any) => getAuthService()?.login(credentials), + register: (data: any) => getAuthService()?.register(data), + logout: () => getAuthService()?.logout(), + updatePassword: (current: string, newPass: string) => getAuthService()?.updatePassword(current, newPass), + updateProfile: (data: any) => getAuthService()?.updateProfile(data), + checkAuth: () => getAuthService()?.checkAuth() +} \ No newline at end of file diff --git a/src/core/services/PaymentService.ts b/src/core/services/PaymentService.ts index 8cf010c..ad35dde 100644 --- a/src/core/services/PaymentService.ts +++ b/src/core/services/PaymentService.ts @@ -51,82 +51,23 @@ export class PaymentService extends BaseService { this.debug('PaymentService initialized with clean state') } - /** - * Get global auth composable - */ - private async getGlobalAuth() { - try { - // Use async dynamic import to avoid circular dependencies - const { auth } = await import('@/composables/useAuth') - return auth - } catch (error) { - this.debug('Could not access global auth:', error) - return null - } - } /** - * Get user wallets from authenticated user (using dual auth detection) - */ - async getUserWalletsAsync() { - // Check both injected auth service AND global auth composable - const hasAuthService = this.authService?.user?.value?.wallets - const globalAuth = await this.getGlobalAuth() - const hasGlobalAuth = globalAuth?.currentUser?.value?.wallets - - const wallets = hasAuthService || hasGlobalAuth || [] - - this.debug('Getting user wallets:', { - hasAuthService: !!hasAuthService, - hasGlobalAuth: !!hasGlobalAuth, - walletsCount: Array.isArray(wallets) ? wallets.length : 0, - walletsSource: hasAuthService ? 'authService' : (hasGlobalAuth ? 'globalAuth' : 'none') - }) - - return wallets - } - - /** - * Get user wallets from authenticated user (synchronous fallback) + * Get user wallets from authenticated user */ get userWallets() { - // Fallback to just auth service for synchronous access return this.authService?.user?.value?.wallets || [] } /** - * Check if user has any wallet with balance (async version) - */ - async hasWalletWithBalanceAsync(): Promise { - const wallets = await this.getUserWalletsAsync() - return wallets.some((wallet: any) => wallet.balance_msat > 0) - } - - /** - * Check if user has any wallet with balance (synchronous fallback) + * Check if user has any wallet with balance */ get hasWalletWithBalance(): boolean { return this.userWallets.some((wallet: any) => wallet.balance_msat > 0) } /** - * Find wallet with sufficient balance for payment (async version) - */ - async getWalletWithBalanceAsync(requiredAmountSats?: number): Promise { - const wallets = await this.getUserWalletsAsync() - if (!wallets.length) return null - - if (requiredAmountSats) { - // Convert sats to msat for comparison - const requiredMsat = requiredAmountSats * 1000 - return wallets.find((wallet: any) => wallet.balance_msat >= requiredMsat) - } - - return wallets.find((wallet: any) => wallet.balance_msat > 0) - } - - /** - * Find wallet with sufficient balance for payment (synchronous fallback) + * Find wallet with sufficient balance for payment */ getWalletWithBalance(requiredAmountSats?: number): any | null { const wallets = this.userWallets @@ -197,26 +138,14 @@ export class PaymentService extends BaseService { requiredAmountSats?: number, options: PaymentOptions = {} ): Promise { - // Check authentication using dual auth detection - const hasAuthService = this.authService?.isAuthenticated?.value && this.authService?.user?.value - const globalAuth = await this.getGlobalAuth() - const hasGlobalAuth = globalAuth?.isAuthenticated?.value && globalAuth?.currentUser?.value - - if (!hasAuthService && !hasGlobalAuth) { - this.debug('Payment failed - user not authenticated:', { - hasAuthService: !!hasAuthService, - hasGlobalAuth: !!hasGlobalAuth - }) + // Check authentication + if (!this.authService?.isAuthenticated?.value || !this.authService?.user?.value) { throw new Error('User must be authenticated to pay with wallet') } - // Find suitable wallet using async version for proper dual auth detection - const wallet = await this.getWalletWithBalanceAsync(requiredAmountSats) + // Find suitable wallet + const wallet = this.getWalletWithBalance(requiredAmountSats) if (!wallet) { - this.debug('No wallet with sufficient balance found:', { - requiredAmountSats, - walletsAvailable: (await this.getUserWalletsAsync()).length - }) throw new Error('No wallet with sufficient balance found') } @@ -307,8 +236,8 @@ export class PaymentService extends BaseService { return null } - // Try wallet payment first if user has balance (use async check for proper dual auth detection) - if (await this.hasWalletWithBalanceAsync()) { + // Try wallet payment first if user has balance + if (this.hasWalletWithBalance) { try { return await this.payWithWallet( paymentRequest, @@ -319,8 +248,6 @@ export class PaymentService extends BaseService { this.debug('Wallet payment failed, offering external wallet option:', error) // Don't throw here, continue to external wallet option } - } else { - this.debug('No wallet with balance available, skipping wallet payment') } // Fallback to external wallet diff --git a/src/modules/base/auth/auth-service.ts b/src/modules/base/auth/auth-service.ts index 0cac8dc..c7c6d13 100644 --- a/src/modules/base/auth/auth-service.ts +++ b/src/modules/base/auth/auth-service.ts @@ -1,8 +1,8 @@ // Auth service for LNbits integration -import { ref } from 'vue' +import { ref, computed } from 'vue' import { BaseService } from '@/core/base/BaseService' import { eventBus } from '@/core/event-bus' -import { lnbitsAPI, type LoginCredentials, type RegisterData } from '@/lib/api/lnbits' +import { lnbitsAPI, type LoginCredentials, type RegisterData, type User } from '@/lib/api/lnbits' export class AuthService extends BaseService { // Service metadata @@ -14,8 +14,23 @@ export class AuthService extends BaseService { // Public state public isAuthenticated = ref(false) - public user = ref(null) + public user = ref(null) public isLoading = ref(false) + public error = ref(null) + + // Computed properties for compatibility with global auth + public currentUser = computed(() => this.user.value) + public userDisplay = computed(() => { + if (!this.user.value) return null + + return { + name: this.user.value.username || this.user.value.email || 'Anonymous', + username: this.user.value.username, + email: this.user.value.email, + id: this.user.value.id, + shortId: this.user.value.id.slice(0, 8) + '...' + this.user.value.id.slice(-8) + } + }) /** * Service-specific initialization (called by BaseService) @@ -104,10 +119,11 @@ export class AuthService extends BaseService { } } - logout(): void { + async logout(): Promise { lnbitsAPI.logout() this.user.value = null this.isAuthenticated.value = false + this.error.value = null eventBus.emit('auth:logout', {}, 'auth-service') } @@ -116,6 +132,37 @@ export class AuthService extends BaseService { // Re-fetch user data from API await this.checkAuth() } + + async initialize(): Promise { + // Alias for checkAuth for compatibility + await this.checkAuth() + } + + async updatePassword(currentPassword: string, newPassword: string): Promise { + try { + this.isLoading.value = true + const updatedUser = await lnbitsAPI.updatePassword(currentPassword, newPassword) + this.user.value = updatedUser + } catch (error) { + const err = this.handleError(error, 'updatePassword') + throw err + } finally { + this.isLoading.value = false + } + } + + async updateProfile(data: Partial): Promise { + try { + this.isLoading.value = true + const updatedUser = await lnbitsAPI.updateProfile(data) + this.user.value = updatedUser + } catch (error) { + const err = this.handleError(error, 'updateProfile') + throw err + } finally { + this.isLoading.value = false + } + } /** * Cleanup when service is disposed diff --git a/src/modules/chat/services/chat-service.ts b/src/modules/chat/services/chat-service.ts index 29919db..503ddc4 100644 --- a/src/modules/chat/services/chat-service.ts +++ b/src/modules/chat/services/chat-service.ts @@ -5,8 +5,6 @@ import { nip04, finalizeEvent, type Event, type EventTemplate } from 'nostr-tool import type { ChatMessage, ChatPeer, UnreadMessageData, ChatConfig } from '../types' import { getAuthToken } from '@/lib/config/lnbits' import { config } from '@/lib/config' - - export class ChatService extends BaseService { // Service metadata protected readonly metadata = { @@ -14,7 +12,6 @@ export class ChatService extends BaseService { version: '1.0.0', dependencies: ['RelayHub', 'AuthService', 'VisibilityService', 'StorageService'] } - // Service-specific state private messages = ref>(new Map()) private peers = ref>(new Map()) @@ -24,80 +21,59 @@ export class ChatService extends BaseService { private visibilityUnsubscribe?: () => void private isFullyInitialized = false private authCheckInterval?: ReturnType - constructor(config: ChatConfig) { super() this.config = config this.loadPeersFromStorage() } - // Register market message handler for forwarding market-related DMs setMarketMessageHandler(handler: (event: any) => Promise) { this.marketMessageHandler = handler } - /** * Service-specific initialization (called by BaseService) */ protected async onInitialize(): Promise { this.debug('Chat service onInitialize called') - // Check both injected auth service AND global auth composable - const { auth } = await import('@/composables/useAuth') + // Removed dual auth import const hasAuthService = this.authService?.user?.value?.pubkey - const hasGlobalAuth = auth.currentUser.value?.pubkey - this.debug('Auth detection:', { hasAuthService: !!hasAuthService, - hasGlobalAuth: !!hasGlobalAuth, - authServicePubkey: hasAuthService ? hasAuthService.substring(0, 10) + '...' : null, - globalAuthPubkey: hasGlobalAuth ? hasGlobalAuth.substring(0, 10) + '...' : null + authServicePubkey: hasAuthService ? hasAuthService.substring(0, 10) : 'none', }) - - if (!hasAuthService && !hasGlobalAuth) { + if (!this.authService?.user?.value) { this.debug('User not authenticated yet, deferring full initialization with periodic check') - // Listen for auth events to complete initialization when user logs in const unsubscribe = eventBus.on('auth:login', async () => { this.debug('Auth login detected, completing chat initialization...') unsubscribe() - if (this.authCheckInterval) { clearInterval(this.authCheckInterval) this.authCheckInterval = undefined } - // Re-inject dependencies and complete initialization await this.waitForDependencies() await this.completeInitialization() }) - // Also check periodically in case we missed the auth event this.authCheckInterval = setInterval(async () => { - const { auth } = await import('@/composables/useAuth') - const hasAuthService = this.authService?.user?.value?.pubkey - const hasGlobalAuth = auth.currentUser.value?.pubkey - - if (hasAuthService || hasGlobalAuth) { + // Removed dual auth import + if (this.authService?.user?.value?.pubkey) { this.debug('Auth detected via periodic check, completing initialization') - if (this.authCheckInterval) { clearInterval(this.authCheckInterval) this.authCheckInterval = undefined } - unsubscribe() await this.waitForDependencies() await this.completeInitialization() } }, 2000) // Check every 2 seconds - return } - await this.completeInitialization() } - /** * Complete the initialization once all dependencies are available */ @@ -106,68 +82,52 @@ export class ChatService extends BaseService { this.debug('Chat service already fully initialized, skipping') return } - this.debug('Completing chat service initialization...') - // Load peers from storage first this.loadPeersFromStorage() - // Load peers from API await this.loadPeersFromAPI().catch(error => { console.warn('Failed to load peers from API:', error) }) - // Initialize message handling (subscription + history loading) await this.initializeMessageHandling() - // Register with visibility service this.registerWithVisibilityService() - this.isFullyInitialized = true this.debug('Chat service fully initialized and ready!') } - - private isFullyInitialized = false - + // Initialize message handling (subscription + history loading) async initializeMessageHandling(): Promise { // Set up real-time subscription await this.setupMessageSubscription() - // Load message history for known peers await this.loadMessageHistory() } - // Computed properties get allPeers() { return computed(() => Array.from(this.peers.value.values())) } - get totalUnreadCount() { return computed(() => { return Array.from(this.peers.value.values()) .reduce((total, peer) => total + peer.unreadCount, 0) }) } - get isReady() { return this.isInitialized } - // Get messages for a specific peer getMessages(peerPubkey: string): ChatMessage[] { return this.messages.value.get(peerPubkey) || [] } - // Get peer by pubkey getPeer(pubkey: string): ChatPeer | undefined { return this.peers.value.get(pubkey) } - // Add or update a peer addPeer(pubkey: string, name?: string): ChatPeer { let peer = this.peers.value.get(pubkey) - if (!peer) { peer = { pubkey, @@ -175,61 +135,48 @@ export class ChatService extends BaseService { unreadCount: 0, lastSeen: Date.now() } - this.peers.value.set(pubkey, peer) this.savePeersToStorage() - eventBus.emit('chat:peer-added', { peer }, 'chat-service') } else if (name && name !== peer.name) { peer.name = name this.savePeersToStorage() } - return peer } - // Add a message addMessage(peerPubkey: string, message: ChatMessage): void { if (!this.messages.value.has(peerPubkey)) { this.messages.value.set(peerPubkey, []) } - const peerMessages = this.messages.value.get(peerPubkey)! - // Avoid duplicates if (!peerMessages.some(m => m.id === message.id)) { peerMessages.push(message) - // Sort by timestamp peerMessages.sort((a, b) => a.created_at - b.created_at) - // Limit message count if (peerMessages.length > this.config.maxMessages) { peerMessages.splice(0, peerMessages.length - this.config.maxMessages) } - // Update peer info const peer = this.addPeer(peerPubkey) peer.lastMessage = message peer.lastSeen = Date.now() - // Update unread count if message is not sent by us if (!message.sent) { this.updateUnreadCount(peerPubkey, message) } - // Emit events const eventType = message.sent ? 'chat:message-sent' : 'chat:message-received' eventBus.emit(eventType, { message, peerPubkey }, 'chat-service') } } - // Mark messages as read for a peer markAsRead(peerPubkey: string): void { const peer = this.peers.value.get(peerPubkey) if (peer && peer.unreadCount > 0) { peer.unreadCount = 0 - // Save unread state const unreadData: UnreadMessageData = { lastReadTimestamp: Date.now(), @@ -237,7 +184,6 @@ export class ChatService extends BaseService { processedMessageIds: new Set() } this.saveUnreadData(peerPubkey, unreadData) - eventBus.emit('chat:unread-count-changed', { peerPubkey, count: 0, @@ -245,61 +191,46 @@ export class ChatService extends BaseService { }, 'chat-service') } } - // Refresh peers from API async refreshPeers(): Promise { // Check if we should trigger full initialization - const { auth } = await import('@/composables/useAuth') - const hasAuth = this.authService?.user?.value?.pubkey || auth.currentUser.value?.pubkey - + // Removed dual auth import + const hasAuth = this.authService?.user?.value?.pubkey if (!this.isFullyInitialized && hasAuth) { console.log('💬 Refresh peers triggered full initialization') await this.completeInitialization() } - return this.loadPeersFromAPI() } - // Check if services are available for messaging private async checkServicesAvailable(): Promise<{ relayHub: any; authService: any; userPubkey: string; userPrivkey: string } | null> { // Check both injected auth service AND global auth composable - const { auth } = await import('@/composables/useAuth') - const hasAuthService = this.authService?.user?.value?.prvkey - const hasGlobalAuth = auth.currentUser.value?.prvkey - - const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey - const userPrivkey = hasAuthService ? this.authService.user.value.prvkey : auth.currentUser.value?.prvkey - - if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) { + // Removed dual auth import + const userPubkey = this.authService?.user?.value?.pubkey + const userPrivkey = this.authService?.user?.value?.prvkey + if (!this.relayHub || (!this.authService?.user?.value)) { return null } - if (!this.relayHub.isConnected) { return null } - return { relayHub: this.relayHub, - authService: this.authService || auth, + authService: this.authService, userPubkey: userPubkey!, userPrivkey: userPrivkey! } } - // Send a message async sendMessage(peerPubkey: string, content: string): Promise { try { const services = await this.checkServicesAvailable() - if (!services) { throw new Error('Chat services not ready. Please wait for connection to establish.') } - const { relayHub, userPrivkey, userPubkey } = services - // Encrypt the message using NIP-04 const encryptedContent = await nip04.encrypt(userPrivkey, peerPubkey, content) - // Create Nostr event for the encrypted message (kind 4 = encrypted direct message) const eventTemplate: EventTemplate = { kind: 4, @@ -307,10 +238,9 @@ export class ChatService extends BaseService { tags: [['p', peerPubkey]], content: encryptedContent } - - // Finalize the event with signature - const signedEvent = finalizeEvent(eventTemplate, userPrivkey) - + // Finalize the event with signature + const privkeyBytes = this.hexToUint8Array(userPrivkey) + const signedEvent = finalizeEvent(eventTemplate, privkeyBytes) // Create local message for immediate display const message: ChatMessage = { id: signedEvent.id, @@ -319,36 +249,40 @@ export class ChatService extends BaseService { sent: true, pubkey: userPubkey } - // Add to local messages immediately this.addMessage(peerPubkey, message) - // Publish to Nostr relays const result = await relayHub.publishEvent(signedEvent) console.log('Message published to relays:', { success: result.success, total: result.total }) - } catch (error) { console.error('Failed to send message:', error) throw error } } - // Private methods + + /** + * Convert hex string to Uint8Array (browser-compatible) + */ + private 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.substr(i, 2), 16) + } + return bytes + } + private updateUnreadCount(peerPubkey: string, message: ChatMessage): void { const unreadData = this.getUnreadData(peerPubkey) - if (!unreadData.processedMessageIds.has(message.id)) { unreadData.processedMessageIds.add(message.id) unreadData.unreadCount++ - const peer = this.peers.value.get(peerPubkey) if (peer) { peer.unreadCount = unreadData.unreadCount this.savePeersToStorage() } - this.saveUnreadData(peerPubkey, unreadData) - eventBus.emit('chat:unread-count-changed', { peerPubkey, count: unreadData.unreadCount, @@ -356,20 +290,17 @@ export class ChatService extends BaseService { }, 'chat-service') } } - private getUnreadData(peerPubkey: string): UnreadMessageData { const data = this.storageService.getUserData(`chat-unread-messages-${peerPubkey}`, { lastReadTimestamp: 0, unreadCount: 0, processedMessageIds: [] }) - return { ...data, processedMessageIds: new Set(data.processedMessageIds || []) } } - private saveUnreadData(peerPubkey: string, data: UnreadMessageData): void { const serializable = { ...data, @@ -377,7 +308,6 @@ export class ChatService extends BaseService { } this.storageService.setUserData(`chat-unread-messages-${peerPubkey}`, serializable) } - // Load peers from API async loadPeersFromAPI(): Promise { try { @@ -386,38 +316,31 @@ export class ChatService extends BaseService { console.warn('💬 No authentication token found for loading peers from API') throw new Error('No authentication token found') } - const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006' console.log('💬 Loading peers from API:', `${API_BASE_URL}/api/v1/auth/nostr/pubkeys`) - const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/pubkeys`, { headers: { 'Authorization': `Bearer ${authToken}`, 'Content-Type': 'application/json' } }) - if (!response.ok) { const errorText = await response.text() console.error('💬 API response error:', response.status, errorText) throw new Error(`Failed to load peers: ${response.status} - ${errorText}`) } - const data = await response.json() console.log('💬 API returned', data?.length || 0, 'peers') - if (!Array.isArray(data)) { console.warn('💬 Invalid API response format - expected array, got:', typeof data) return } - // Don't clear existing peers - merge instead data.forEach((peer: any) => { if (!peer.pubkey) { console.warn('💬 Skipping peer without pubkey:', peer) return } - const chatPeer: ChatPeer = { pubkey: peer.pubkey, name: peer.username || `User ${peer.pubkey.slice(0, 8)}`, @@ -426,18 +349,14 @@ export class ChatService extends BaseService { } this.peers.value.set(peer.pubkey, chatPeer) }) - // Save to storage this.savePeersToStorage() - console.log(`✅ Loaded ${data.length} peers from API, total peers now: ${this.peers.value.size}`) - } catch (error) { console.error('❌ Failed to load peers from API:', error) // Don't re-throw - peers from storage are still available } } - private loadPeersFromStorage(): void { // Skip loading peers in constructor as StorageService may not be available yet // This will be called later during initialization when dependencies are ready @@ -445,7 +364,6 @@ export class ChatService extends BaseService { this.debug('Skipping peer loading from storage - not initialized or storage unavailable') return } - try { const peersArray = this.storageService.getUserData('chat-peers', []) as ChatPeer[] console.log('💬 Loading', peersArray.length, 'peers from storage') @@ -456,41 +374,32 @@ export class ChatService extends BaseService { console.warn('💬 Failed to load peers from storage:', error) } } - private savePeersToStorage(): void { const peersArray = Array.from(this.peers.value.values()) this.storageService.setUserData('chat-peers', peersArray) } - // Load message history for known peers private async loadMessageHistory(): Promise { try { // Check both injected auth service AND global auth composable - const { auth } = await import('@/composables/useAuth') - const hasAuthService = this.authService?.user?.value?.pubkey - const hasGlobalAuth = auth.currentUser.value?.pubkey - - const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey - const userPrivkey = hasAuthService ? this.authService.user.value.prvkey : auth.currentUser.value?.prvkey - - if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) { + // Removed dual auth import + // const hasAuthService = this.authService?.user?.value?.pubkey + const userPubkey = this.authService?.user?.value?.pubkey + const userPrivkey = this.authService?.user?.value?.prvkey + if (!this.relayHub || (!this.authService?.user?.value)) { console.warn('Cannot load message history: missing services') return } - if (!userPubkey || !userPrivkey) { console.warn('Cannot load message history: missing user keys') return } const peerPubkeys = Array.from(this.peers.value.keys()) - if (peerPubkeys.length === 0) { console.log('No peers to load message history for') return } - console.log('Loading message history for', peerPubkeys.length, 'peers') - // Query historical messages (kind 4) to/from known peers // We need separate queries for sent vs received messages due to different tagging const receivedEvents = await this.relayHub.queryEvents([ @@ -501,7 +410,6 @@ export class ChatService extends BaseService { limit: 100 } ]) - const sentEvents = await this.relayHub.queryEvents([ { kinds: [4], @@ -510,12 +418,9 @@ export class ChatService extends BaseService { limit: 100 } ]) - const events = [...receivedEvents, ...sentEvents] .sort((a, b) => a.created_at - b.created_at) // Sort by timestamp - console.log('Found', events.length, 'historical messages:', receivedEvents.length, 'received,', sentEvents.length, 'sent') - // Process historical messages for (const event of events) { try { @@ -523,12 +428,9 @@ export class ChatService extends BaseService { const peerPubkey = isFromUs ? event.tags.find((tag: string[]) => tag[0] === 'p')?.[1] // Get recipient from tag : event.pubkey // Sender is the peer - if (!peerPubkey || peerPubkey === userPubkey) continue - // Decrypt the message const decryptedContent = await nip04.decrypt(userPrivkey, peerPubkey, event.content) - // Create a chat message const message: ChatMessage = { id: event.id, @@ -537,46 +439,36 @@ export class ChatService extends BaseService { sent: isFromUs, pubkey: event.pubkey } - // Add the message (will avoid duplicates) this.addMessage(peerPubkey, message) - } catch (error) { console.error('Failed to decrypt historical message:', error) } } - console.log('Message history loaded successfully') - } catch (error) { console.error('Failed to load message history:', error) } } - // Setup subscription for incoming messages private async setupMessageSubscription(): Promise { try { // Check both injected auth service AND global auth composable - const { auth } = await import('@/composables/useAuth') - const hasAuthService = this.authService?.user?.value?.pubkey - const hasGlobalAuth = auth.currentUser.value?.pubkey - const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey - + // Removed dual auth import + // const hasAuthService = this.authService?.user?.value?.pubkey + const userPubkey = this.authService?.user?.value?.pubkey this.debug('Setup message subscription auth check:', { - hasAuthService: !!hasAuthService, - hasGlobalAuth: !!hasGlobalAuth, + hasAuthService: !!this.authService?.user?.value, hasRelayHub: !!this.relayHub, relayHubConnected: this.relayHub?.isConnected, - userPubkey: userPubkey ? userPubkey.substring(0, 10) + '...' : null + userPubkey: userPubkey ? userPubkey.substring(0, 10) : 'none', }) - - if (!this.relayHub || (!hasAuthService && !hasGlobalAuth)) { + if (!this.relayHub || (!this.authService?.user?.value)) { console.warn('💬 Cannot setup message subscription: missing services') // Retry after 2 seconds setTimeout(() => this.setupMessageSubscription(), 2000) return } - if (!this.relayHub.isConnected) { console.warn('💬 RelayHub not connected, waiting for connection...') // Listen for connection event @@ -588,13 +480,11 @@ export class ChatService extends BaseService { setTimeout(() => this.setupMessageSubscription(), 5000) return } - if (!userPubkey) { console.warn('💬 No user pubkey available for subscription') setTimeout(() => this.setupMessageSubscription(), 2000) return } - // Subscribe to encrypted direct messages (kind 4) addressed to this user this.subscriptionUnsubscriber = this.relayHub.subscribe({ id: 'chat-messages', @@ -609,23 +499,19 @@ export class ChatService extends BaseService { if (event.pubkey === userPubkey) { return } - await this.processIncomingMessage(event) }, onEose: () => { console.log('💬 Chat message subscription EOSE received') } }) - console.log('💬 Chat message subscription set up successfully for pubkey:', userPubkey.substring(0, 10) + '...') - } catch (error) { console.error('💬 Failed to setup message subscription:', error) // Retry after delay setTimeout(() => this.setupMessageSubscription(), 3000) } } - /** * Register with VisibilityService for connection management */ @@ -634,42 +520,34 @@ export class ChatService extends BaseService { this.debug('VisibilityService not available') return } - this.visibilityUnsubscribe = this.visibilityService.registerService( this.metadata.name, async () => this.handleAppResume(), async () => this.handleAppPause() ) - this.debug('Registered with VisibilityService') } - /** * Handle app resuming from visibility change */ private async handleAppResume(): Promise { this.debug('App resumed - checking chat connections') - // Check if subscription is still active if (!this.subscriptionUnsubscriber) { this.debug('Chat subscription lost, re-establishing...') this.setupMessageSubscription() } - // Check if we need to sync missed messages await this.syncMissedMessages() } - /** * Handle app pausing from visibility change */ private async handleAppPause(): Promise { this.debug('App paused - chat subscription will be maintained for quick resume') - // Don't immediately unsubscribe - let RelayHub handle connection management // Subscriptions will be restored automatically on resume if needed } - /** * Sync any messages that might have been missed while app was hidden */ @@ -678,39 +556,30 @@ export class ChatService extends BaseService { // For each peer, try to load recent messages const peers = Array.from(this.peers.value.values()) const syncPromises = peers.map(peer => this.loadRecentMessagesForPeer(peer.pubkey)) - await Promise.allSettled(syncPromises) this.debug('Missed messages sync completed') - } catch (error) { console.warn('Failed to sync missed messages:', error) } } - /** * Process an incoming message event */ private async processIncomingMessage(event: any): Promise { try { // Check both injected auth service AND global auth composable - const { auth } = await import('@/composables/useAuth') - const hasAuthService = this.authService?.user?.value?.pubkey - const hasGlobalAuth = auth.currentUser.value?.pubkey - - const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey - const userPrivkey = hasAuthService ? this.authService.user.value.prvkey : auth.currentUser.value?.prvkey - + // Removed dual auth import + // const hasAuthService = this.authService?.user?.value?.pubkey + const userPubkey = this.authService?.user?.value?.pubkey + const userPrivkey = this.authService?.user?.value?.prvkey if (!userPubkey || !userPrivkey) { console.warn('Cannot process message: user not authenticated') return } - // Get sender pubkey from event const senderPubkey = event.pubkey - // Decrypt the message content const decryptedContent = await nip04.decrypt(userPrivkey, senderPubkey, event.content) - // Check if this is a market-related message let isMarketMessage = false try { @@ -718,7 +587,6 @@ export class ChatService extends BaseService { if (parsedContent.type === 1 || parsedContent.type === 2) { // This is a market order message isMarketMessage = true - // Forward to market handler if (this.marketMessageHandler) { await this.marketMessageHandler(event) @@ -730,12 +598,10 @@ export class ChatService extends BaseService { } catch (e) { // Not JSON or not a market message, treat as regular chat } - // Process as chat message regardless (market messages should also appear in chat) { // Format the content for display based on whether it's a market message let displayContent = decryptedContent - if (isMarketMessage) { try { const parsedContent = JSON.parse(decryptedContent) @@ -749,14 +615,12 @@ export class ChatService extends BaseService { else if (parsedContent.paid === false) status.push('⏳ Payment Pending') if (parsedContent.shipped === true) status.push('📦 Shipped') else if (parsedContent.shipped === false) status.push('🔄 Processing') - displayContent = `📋 Order Update: ${parsedContent.id}\n${status.join(' | ')}\n${parsedContent.message || ''}` } } catch (e) { // Fallback to raw content if parsing fails } } - // Create a chat message const message: ChatMessage = { id: event.id, @@ -765,37 +629,27 @@ export class ChatService extends BaseService { sent: false, pubkey: senderPubkey } - // Ensure we have a peer record for the sender this.addPeer(senderPubkey) - // Add the message this.addMessage(senderPubkey, message) - console.log('Received encrypted chat message from:', senderPubkey.slice(0, 8)) } - } catch (error) { console.error('Failed to process incoming message:', error) } } - /** * Load recent messages for a specific peer */ private async loadRecentMessagesForPeer(peerPubkey: string): Promise { // Check both injected auth service AND global auth composable - const { auth } = await import('@/composables/useAuth') - const hasAuthService = this.authService?.user?.value?.pubkey - const hasGlobalAuth = auth.currentUser.value?.pubkey - - const userPubkey = hasAuthService ? this.authService.user.value.pubkey : auth.currentUser.value?.pubkey + // Removed dual auth import + const userPubkey = this.authService?.user?.value?.pubkey if (!userPubkey || !this.relayHub) return - try { // Get last 10 messages from the last hour for this peer const oneHourAgo = Math.floor(Date.now() / 1000) - 3600 - const events = await this.relayHub.queryEvents([ { kinds: [4], // Encrypted DMs @@ -812,17 +666,14 @@ export class ChatService extends BaseService { limit: 10 } ]) - // Process any new messages for (const event of events) { await this.processIncomingMessage(event) } - } catch (error) { this.debug(`Failed to load recent messages for peer ${peerPubkey.slice(0, 8)}:`, error) } } - /** * Cleanup when service is disposed (overrides BaseService) */ @@ -832,26 +683,21 @@ export class ChatService extends BaseService { clearInterval(this.authCheckInterval) this.authCheckInterval = undefined } - // Unregister from visibility service if (this.visibilityUnsubscribe) { this.visibilityUnsubscribe() this.visibilityUnsubscribe = undefined } - // Unsubscribe from message subscription if (this.subscriptionUnsubscriber) { this.subscriptionUnsubscriber() this.subscriptionUnsubscriber = undefined } - this.messages.value.clear() this.peers.value.clear() this.isFullyInitialized = false - this.debug('Chat service disposed') } - /** * Legacy destroy method for backward compatibility */ diff --git a/src/modules/events/components/PurchaseTicketDialog.vue b/src/modules/events/components/PurchaseTicketDialog.vue index 4c93ace..7493c63 100644 --- a/src/modules/events/components/PurchaseTicketDialog.vue +++ b/src/modules/events/components/PurchaseTicketDialog.vue @@ -1,11 +1,11 @@