From daa96566805e61e5429bc1e050477cf39a598746 Mon Sep 17 00:00:00 2001 From: padreug Date: Fri, 5 Sep 2025 02:48:47 +0200 Subject: [PATCH] Implement LNbits integration in AuthService and enhance ChatComponent for improved user experience - Refactor AuthService to integrate LNbits authentication, including fetching user data from the API and handling token validation. - Update ChatComponent to reflect changes in peer management, replacing user_id with pubkey and username with name for better clarity. - Enhance connection status indicators in ChatComponent for improved user feedback during chat initialization. --- src/modules/base/auth/auth-service.ts | 79 +++-- src/modules/chat/components/ChatComponent.vue | 127 ++----- src/modules/chat/composables/useChat.ts | 18 +- src/modules/chat/services/chat-service.ts | 326 +++++++++++++++++- 4 files changed, 421 insertions(+), 129 deletions(-) diff --git a/src/modules/base/auth/auth-service.ts b/src/modules/base/auth/auth-service.ts index 11b8d61..6a3d257 100644 --- a/src/modules/base/auth/auth-service.ts +++ b/src/modules/base/auth/auth-service.ts @@ -1,6 +1,8 @@ -// Copy the existing auth logic into a service class +// Auth service for LNbits integration import { ref } from 'vue' import { eventBus } from '@/core/event-bus' +import { getAuthToken } from '@/lib/config/lnbits' +import { config } from '@/lib/config' export class AuthService { public isAuthenticated = ref(false) @@ -10,33 +12,64 @@ export class AuthService { async initialize(): Promise { console.log('🔑 Initializing auth service...') - // Check for existing auth state - this.checkAuth() + // Check for existing auth state and fetch user data + await this.checkAuth() if (this.isAuthenticated.value) { eventBus.emit('auth:login', { user: this.user.value }, 'auth-service') } } - checkAuth(): boolean { - // Implement your existing auth check logic here - // For now, we'll use a simple localStorage check - const authData = localStorage.getItem('auth') - if (authData) { - try { - const parsed = JSON.parse(authData) - this.isAuthenticated.value = true - this.user.value = parsed - return true - } catch (error) { - console.error('Invalid auth data in localStorage:', error) - this.logout() - } - } + async checkAuth(): Promise { + const authToken = getAuthToken() - this.isAuthenticated.value = false - this.user.value = null - return false + if (!authToken) { + console.log('🔑 No auth token found - user needs to login') + this.isAuthenticated.value = false + this.user.value = null + return false + } + + // Fetch current user data from API + try { + this.isLoading.value = true + const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006' + const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/me`, { + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + if (response.status === 401) { + console.log('🔑 Auth token invalid - user needs to login') + this.logout() + return false + } + console.warn(`🔑 Failed to fetch user data: ${response.status} - authentication may not be properly configured`) + this.isAuthenticated.value = false + this.user.value = null + return false + } + + const userData = await response.json() + + this.user.value = userData + this.isAuthenticated.value = true + + console.log('🔑 User authenticated:', userData.username || userData.id, userData.pubkey?.slice(0, 8)) + + return true + + } catch (error) { + console.warn('🔑 Authentication check failed:', error) + this.isAuthenticated.value = false + this.user.value = null + return false + } finally { + this.isLoading.value = false + } } async login(credentials: any): Promise { @@ -71,8 +104,8 @@ export class AuthService { } async refresh(): Promise { - // Implement token refresh logic if needed - console.log('Refreshing auth token...') + // Re-fetch user data from API + await this.checkAuth() } } diff --git a/src/modules/chat/components/ChatComponent.vue b/src/modules/chat/components/ChatComponent.vue index 31d51a1..7e2c27a 100644 --- a/src/modules/chat/components/ChatComponent.vue +++ b/src/modules/chat/components/ChatComponent.vue @@ -7,10 +7,10 @@

Chat

- Connected + Ready - Disconnected + Initializing... @@ -68,11 +68,11 @@

- {{ peer.username || 'Unknown User' }} + {{ peer.name || 'Unknown User' }}

{{ formatPubkey(peer.pubkey) }} @@ -119,7 +119,7 @@ {{ selectedPeer ? getPeerInitials(selectedPeer) : 'U' }}

-

{{ selectedPeer?.username || 'Unknown User' }}

+

{{ selectedPeer?.name || 'Unknown User' }}

{{ selectedPeer ? formatPubkey(selectedPeer.pubkey) : '' }}

@@ -127,10 +127,10 @@
- Connected + Ready - Disconnected + Initializing... @@ -192,10 +192,10 @@

Chat

- Connected + Ready - Disconnected + Initializing... @@ -255,11 +255,11 @@

- {{ peer.username || 'Unknown User' }} + {{ peer.name || 'Unknown User' }}

{{ formatPubkey(peer.pubkey) }} @@ -297,7 +297,7 @@ {{ getPeerInitials(selectedPeer) }}

-

{{ selectedPeer.username || 'Unknown User' }}

+

{{ selectedPeer.name || 'Unknown User' }}

{{ formatPubkey(selectedPeer.pubkey) }}

@@ -380,18 +380,14 @@ import { useChat } from '../composables/useChat' import { useFuzzySearch } from '@/composables/useFuzzySearch' // Types -interface Peer { - user_id: string - username: string - pubkey: string -} +import type { ChatPeer } from '../types' // Initialize chat composable const chat = useChat() // State const peers = computed(() => chat.peers.value) -const selectedPeer = ref(null) +const selectedPeer = ref(null) const messageInput = ref('') const isLoading = ref(false) @@ -405,45 +401,23 @@ const isMobile = ref(false) // Get methods and state from chat composable // Note: The modular chat service handles connection and peer management automatically -const isConnected = computed(() => true) // Chat service manages connection -const messages = ref(new Map()) // Local messages map for compatibility +const isConnected = computed(() => chat.isReady.value) // Use chat service ready state const totalUnreadCount = computed(() => chat.totalUnreadCount.value) -// Adapter functions for compatibility with existing code -const connect = async () => {} // Connection handled by chat service -const disconnect = () => {} // Handled by chat service -const subscribeToPeer = async (peer: string) => {} // Handled by chat service -const sendNostrMessage = async (peer: string, content: string) => { - chat.selectPeer(peer) - await chat.sendMessage(content) -} -const onMessageAdded = (callback: Function) => {} // Event handling via chat service -const markMessagesAsRead = (peer: string) => chat.markAsRead(peer) +// Helper functions const getUnreadCount = (peer: string) => { const peerData = chat.peers.value.find(p => p.pubkey === peer) return peerData?.unreadCount || 0 } -const getLatestMessageTimestamp = (peer: string) => { - const msgs = messages.value.get(peer) || [] - return msgs.length > 0 ? msgs[msgs.length - 1].created_at : 0 -} // Computed const currentMessages = computed(() => { - if (!selectedPeer.value) return [] - const peerMessages = messages.value.get(selectedPeer.value.pubkey) || [] - - // Sort messages by timestamp (oldest first) to ensure chronological order - const sortedMessages = [...peerMessages].sort((a, b) => a.created_at - b.created_at) - - return sortedMessages + return chat.currentMessages.value }) -// Sort peers by latest message timestamp (newest first) and unread status +// Sort peers by unread count and name const sortedPeers = computed(() => { const sorted = [...peers.value].sort((a, b) => { - const aTimestamp = getLatestMessageTimestamp(a.pubkey) - const bTimestamp = getLatestMessageTimestamp(b.pubkey) const aUnreadCount = getUnreadCount(a.pubkey) const bUnreadCount = getUnreadCount(b.pubkey) @@ -451,13 +425,8 @@ const sortedPeers = computed(() => { if (aUnreadCount > 0 && bUnreadCount === 0) return -1 if (aUnreadCount === 0 && bUnreadCount > 0) return 1 - // Then, sort by latest message timestamp (newest first) - if (aTimestamp !== bTimestamp) { - return bTimestamp - aTimestamp - } - - // Finally, sort alphabetically by username for peers with same timestamp - return (a.username || '').localeCompare(b.username || '') + // Finally, sort alphabetically by name for peers with same unread status + return (a.name || '').localeCompare(b.name || '') }) return sorted @@ -475,7 +444,7 @@ const { } = useFuzzySearch(sortedPeers, { fuseOptions: { keys: [ - { name: 'username', weight: 0.7 }, // Username has higher weight for better UX + { name: 'name', weight: 0.7 }, // Name has higher weight for better UX { name: 'pubkey', weight: 0.3 } // Pubkey has lower weight but still searchable ], threshold: 0.3, // Fuzzy matching threshold (0.0 = perfect, 1.0 = match anything) @@ -509,31 +478,21 @@ const goBackToPeers = () => { const refreshPeers = async () => { - isLoading.value = true - try { - // Peers are loaded automatically by the chat service - } catch (error) { - console.error('Failed to refresh peers:', error) - } finally { - isLoading.value = false - } + await chat.refreshPeers() } -const selectPeer = async (peer: Peer) => { +const selectPeer = async (peer: ChatPeer) => { selectedPeer.value = peer messageInput.value = '' - // Mark messages as read for this peer - markMessagesAsRead(peer.pubkey) + // Use the modular chat service + chat.selectPeer(peer.pubkey) // On mobile, show chat view if (isMobile.value) { showChat.value = true } - // Subscribe to messages from this peer - await subscribeToPeer(peer.pubkey) - // Scroll to bottom to show latest messages when selecting a peer nextTick(() => { scrollToBottom() @@ -544,7 +503,7 @@ const sendMessage = async () => { if (!selectedPeer.value || !messageInput.value.trim()) return try { - await sendNostrMessage(selectedPeer.value.pubkey, messageInput.value) + await chat.sendMessage(messageInput.value) messageInput.value = '' // Scroll to bottom @@ -579,14 +538,14 @@ const formatTime = (timestamp: number) => { }) } -const getPeerAvatar = (_peer: Peer) => { +const getPeerAvatar = (_peer: ChatPeer) => { // You can implement avatar logic here return null } -const getPeerInitials = (peer: Peer) => { - if (peer.username) { - return peer.username.slice(0, 2).toUpperCase() +const getPeerInitials = (peer: ChatPeer) => { + if (peer.name) { + return peer.name.slice(0, 2).toUpperCase() } return peer.pubkey.slice(0, 2).toUpperCase() } @@ -594,33 +553,13 @@ const getPeerInitials = (peer: Peer) => { // Lifecycle -onMounted(async () => { +onMounted(() => { checkMobile() window.addEventListener('resize', checkMobile) - - // Set up message callback - onMessageAdded.value = (peerPubkey: string) => { - if (selectedPeer.value && selectedPeer.value.pubkey === peerPubkey) { - nextTick(() => { - scrollToBottom() - }) - } - } - - // If not connected, connect - if (!isConnected.value) { - await connect() - } - - // If no peers loaded, load them - if (peers.value.length === 0) { - // Peers are loaded automatically by the chat service - } }) onUnmounted(() => { window.removeEventListener('resize', checkMobile) - disconnect() }) // Watch for connection state changes diff --git a/src/modules/chat/composables/useChat.ts b/src/modules/chat/composables/useChat.ts index 736102e..070beee 100644 --- a/src/modules/chat/composables/useChat.ts +++ b/src/modules/chat/composables/useChat.ts @@ -20,6 +20,7 @@ export function useChat() { // Computed properties const peers = computed(() => chatService.allPeers.value) const totalUnreadCount = computed(() => chatService.totalUnreadCount.value) + const isReady = computed(() => chatService.isReady.value) const currentMessages = computed(() => { return selectedPeer.value ? chatService.getMessages(selectedPeer.value) : [] @@ -61,6 +62,19 @@ export function useChat() { chatService.markAsRead(peerPubkey) } + const refreshPeers = async () => { + isLoading.value = true + error.value = null + try { + await chatService.refreshPeers() + } catch (err) { + error.value = err instanceof Error ? err.message : 'Failed to refresh peers' + console.error('Failed to refresh peers:', err) + } finally { + isLoading.value = false + } + } + return { // State selectedPeer, @@ -70,6 +84,7 @@ export function useChat() { // Computed peers, totalUnreadCount, + isReady, currentMessages, currentPeer, @@ -77,6 +92,7 @@ export function useChat() { selectPeer, sendMessage, addPeer, - markAsRead + markAsRead, + refreshPeers } } \ No newline at end of file diff --git a/src/modules/chat/services/chat-service.ts b/src/modules/chat/services/chat-service.ts index b44ea6d..faf51a7 100644 --- a/src/modules/chat/services/chat-service.ts +++ b/src/modules/chat/services/chat-service.ts @@ -1,7 +1,10 @@ import { ref, computed } from 'vue' import { eventBus } from '@/core/event-bus' import { injectService, SERVICE_TOKENS } from '@/core/di-container' +import { nip04, getEventHash, finalizeEvent, type Event, type EventTemplate } from 'nostr-tools' import type { ChatMessage, ChatPeer, UnreadMessageData, ChatConfig } from '../types' +import { getAuthToken } from '@/lib/config/lnbits' +import { config } from '@/lib/config' const UNREAD_MESSAGES_KEY = 'nostr-chat-unread-messages' const PEERS_KEY = 'nostr-chat-peers' @@ -10,10 +13,67 @@ export class ChatService { private messages = ref>(new Map()) private peers = ref>(new Map()) private config: ChatConfig + private subscriptionUnsubscriber?: () => void + private isInitialized = ref(false) constructor(config: ChatConfig) { this.config = config this.loadPeersFromStorage() + + // Defer initialization until services are available + this.deferredInitialization() + } + + // Defer initialization until services are ready + private deferredInitialization(): void { + // Try initialization immediately + this.tryInitialization() + + // Also listen for auth events to re-initialize when user logs in + eventBus.on('auth:login', () => { + console.log('💬 Auth login detected, initializing chat...') + this.tryInitialization() + }) + } + + // Try to initialize services if they're available + private async tryInitialization(): Promise { + try { + const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any + const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any + + if (!relayHub || !authService?.user?.value?.pubkey) { + console.log('💬 Services not ready yet, will retry when auth completes...') + return + } + + console.log('💬 Services ready, initializing chat functionality...') + + // 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() + + // Mark as initialized + this.isInitialized.value = true + console.log('💬 Chat service fully initialized and ready!') + + } catch (error) { + console.error('💬 Failed to initialize chat:', error) + this.isInitialized.value = false + } + } + + // Initialize message handling (subscription + history loading) + async initializeMessageHandling(): Promise { + // Set up real-time subscription + this.setupMessageSubscription() + + // Load message history for known peers + await this.loadMessageHistory() } // Computed properties @@ -28,6 +88,10 @@ export class ChatService { }) } + get isReady() { + return computed(() => this.isInitialized.value) + } + // Get messages for a specific peer getMessages(peerPubkey: string): ChatMessage[] { return this.messages.value.get(peerPubkey) || [] @@ -120,31 +184,70 @@ export class ChatService { } } + // Refresh peers from API + async refreshPeers(): Promise { + return this.loadPeersFromAPI() + } + + // Check if services are available for messaging + private checkServicesAvailable(): { relayHub: any; authService: any } | null { + const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any + const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any + + if (!relayHub || !authService?.user?.value?.prvkey) { + return null + } + + if (!relayHub.isConnected) { + return null + } + + return { relayHub, authService } + } + // Send a message async sendMessage(peerPubkey: string, content: string): Promise { try { - const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) - const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any + const services = this.checkServicesAvailable() - if (!relayHub || !authService?.user?.value?.privkey) { - throw new Error('Required services not available') + if (!services) { + throw new Error('Chat services not ready. Please wait for connection to establish.') + } + + const { relayHub, authService } = services + + const userPrivkey = authService.user.value.prvkey + const userPubkey = authService.user.value.pubkey + + // 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, + created_at: Math.floor(Date.now() / 1000), + tags: [['p', peerPubkey]], + content: encryptedContent } - // Create message + // Finalize the event with signature + const signedEvent = finalizeEvent(eventTemplate, userPrivkey) + + // Create local message for immediate display const message: ChatMessage = { - id: crypto.randomUUID(), + id: signedEvent.id, content, - created_at: Math.floor(Date.now() / 1000), + created_at: signedEvent.created_at, sent: true, - pubkey: authService.user.value.pubkey + pubkey: userPubkey } // Add to local messages immediately this.addMessage(peerPubkey, message) - // TODO: Implement actual Nostr message sending - // This would involve encrypting the message and publishing to relays - console.log('Sending message:', { peerPubkey, content }) + // 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) @@ -209,6 +312,52 @@ export class ChatService { } } + // Load peers from API + async loadPeersFromAPI(): Promise { + try { + const authToken = getAuthToken() + if (!authToken) { + throw new Error('No authentication token found') + } + + const API_BASE_URL = config.api.baseUrl || 'http://localhost:5006' + const response = await fetch(`${API_BASE_URL}/api/v1/auth/nostr/pubkeys`, { + headers: { + 'Authorization': `Bearer ${authToken}`, + 'Content-Type': 'application/json' + } + }) + + if (!response.ok) { + throw new Error(`Failed to load peers: ${response.status}`) + } + + const data = await response.json() + + // Clear existing peers and load from API + this.peers.value.clear() + + data.forEach((peer: any) => { + const chatPeer: ChatPeer = { + pubkey: peer.pubkey, + name: peer.username || `User ${peer.pubkey.slice(0, 8)}`, + unreadCount: 0, + lastSeen: Date.now() + } + this.peers.value.set(peer.pubkey, chatPeer) + }) + + // Save to storage + this.savePeersToStorage() + + console.log(`Loaded ${data.length} peers from API`) + + } catch (error) { + console.error('Failed to load peers from API:', error) + throw error + } + } + private loadPeersFromStorage(): void { try { const stored = localStorage.getItem(PEERS_KEY) @@ -232,8 +381,163 @@ export class ChatService { } } + // Load message history for known peers + private async loadMessageHistory(): Promise { + try { + const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any + const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any + + if (!relayHub || !authService?.user?.value?.pubkey) { + console.warn('Cannot load message history: missing services') + return + } + + const userPubkey = authService.user.value.pubkey + const userPrivkey = authService.user.value.prvkey + 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 + const events = await relayHub.queryEvents([ + { + kinds: [4], + authors: [userPubkey, ...peerPubkeys], // Messages from us or peers + '#p': [userPubkey], // Messages tagged with our pubkey + limit: 100 // Limit to last 100 messages per conversation + } + ]) + + console.log('Found', events.length, 'historical messages') + + // Process historical messages + for (const event of events) { + try { + const isFromUs = event.pubkey === userPubkey + const peerPubkey = isFromUs + ? event.tags.find(tag => 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, + content: decryptedContent, + created_at: event.created_at, + 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 setupMessageSubscription(): void { + try { + const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB) as any + const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE) as any + + if (!relayHub || !authService?.user?.value?.pubkey) { + console.warn('💬 Cannot setup message subscription: missing services') + return + } + + if (!relayHub.isConnected) { + console.warn('💬 RelayHub not connected, waiting for connection...') + // Listen for connection event + relayHub.on('connected', () => { + console.log('💬 RelayHub connected, setting up message subscription...') + this.setupMessageSubscription() + }) + return + } + + const userPubkey = authService.user.value.pubkey + const userPrivkey = authService.user.value.prvkey + + // Subscribe to encrypted direct messages (kind 4) addressed to this user + this.subscriptionUnsubscriber = relayHub.subscribe({ + id: 'chat-messages', + filters: [ + { + kinds: [4], // Encrypted direct messages + '#p': [userPubkey] // Messages tagged with our pubkey + } + ], + onEvent: async (event: Event) => { + try { + // Find the sender's pubkey from the event + const senderPubkey = event.pubkey + + // Skip our own messages + if (senderPubkey === userPubkey) { + return + } + + // Decrypt the message + const decryptedContent = await nip04.decrypt(userPrivkey, senderPubkey, event.content) + + // Create a chat message + const message: ChatMessage = { + id: event.id, + content: decryptedContent, + created_at: event.created_at, + 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 message from:', senderPubkey.slice(0, 8)) + + } catch (error) { + console.error('Failed to decrypt incoming message:', error) + } + }, + onEose: () => { + console.log('Chat message subscription EOSE received') + } + }) + + console.log('Chat message subscription set up successfully') + + } catch (error) { + console.error('Failed to setup message subscription:', error) + } + } + // Cleanup destroy(): void { + // Unsubscribe from message subscription + if (this.subscriptionUnsubscriber) { + this.subscriptionUnsubscriber() + } + this.messages.value.clear() this.peers.value.clear() }