Compare commits
2 commits
26a661891a
...
531de18ad7
| Author | SHA1 | Date | |
|---|---|---|---|
| 531de18ad7 | |||
| 414b79565c |
6 changed files with 17 additions and 300 deletions
|
|
@ -144,9 +144,6 @@ export const SERVICE_TOKENS = {
|
|||
SUBMISSION_SERVICE: Symbol('submissionService'),
|
||||
LINK_PREVIEW_SERVICE: Symbol('linkPreviewService'),
|
||||
|
||||
// Nostr metadata services
|
||||
NOSTR_METADATA_SERVICE: Symbol('nostrMetadataService'),
|
||||
|
||||
// Nostr transport (kind-21000 RPC over relays — LNbits backend)
|
||||
NOSTR_TRANSPORT_SERVICE: Symbol('nostrTransportService'),
|
||||
|
||||
|
|
|
|||
|
|
@ -2,9 +2,7 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { BaseService } from '@/core/base/BaseService'
|
||||
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 {
|
||||
|
|
@ -114,9 +112,6 @@ export class AuthService extends BaseService {
|
|||
|
||||
eventBus.emit('auth:login', { user: userData }, 'auth-service')
|
||||
|
||||
// Auto-broadcast Nostr metadata on login
|
||||
this.broadcastNostrMetadata()
|
||||
|
||||
} catch (error) {
|
||||
const err = this.handleError(error, 'login')
|
||||
eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
|
||||
|
|
@ -138,9 +133,6 @@ export class AuthService extends BaseService {
|
|||
|
||||
eventBus.emit('auth:login', { user: userData }, 'auth-service')
|
||||
|
||||
// Auto-broadcast Nostr metadata on registration
|
||||
this.broadcastNostrMetadata()
|
||||
|
||||
} catch (error) {
|
||||
const err = this.handleError(error, 'register')
|
||||
eventBus.emit('auth:login-failed', { error: err }, 'auth-service')
|
||||
|
|
@ -195,10 +187,9 @@ export class AuthService extends BaseService {
|
|||
prvkey: this.user.value?.prvkey || updatedUser.prvkey
|
||||
}
|
||||
|
||||
// Auto-broadcast Nostr metadata when profile is updated
|
||||
// Note: ProfileSettings component will also manually broadcast,
|
||||
// but this ensures metadata stays in sync even if updated elsewhere
|
||||
this.broadcastNostrMetadata()
|
||||
// Kind-0 metadata is published server-side by lnbits's PATCH /auth handler
|
||||
// (aiolabs/lnbits commit 869f67c3) once the cascade is deployed. The webapp
|
||||
// no longer maintains its own broadcast path.
|
||||
|
||||
} catch (error) {
|
||||
const err = this.handleError(error, 'updateProfile')
|
||||
|
|
@ -208,26 +199,6 @@ export class AuthService extends BaseService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast user metadata to Nostr relays (NIP-01 kind 0)
|
||||
* Called automatically on login, registration, and profile updates
|
||||
*/
|
||||
private async broadcastNostrMetadata(): Promise<void> {
|
||||
try {
|
||||
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
||||
if (metadataService && this.user.value?.pubkey) {
|
||||
// Broadcast in background - don't block login/update
|
||||
metadataService.publishMetadata().catch(error => {
|
||||
console.warn('Failed to broadcast Nostr metadata:', error)
|
||||
// Don't throw - this is a non-critical background operation
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// If service isn't available yet, silently skip
|
||||
console.debug('Nostr metadata service not yet available')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when service is disposed
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -122,32 +122,17 @@
|
|||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex flex-col sm:flex-row gap-3">
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="isUpdating || !isFormValid"
|
||||
class="flex-1"
|
||||
class="w-full"
|
||||
>
|
||||
<span v-if="isUpdating">Updating...</span>
|
||||
<span v-else>Update Profile</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
:disabled="isBroadcasting"
|
||||
@click="broadcastMetadata"
|
||||
class="flex-1"
|
||||
>
|
||||
<Radio class="mr-2 h-4 w-4" :class="{ 'animate-pulse': isBroadcasting }" />
|
||||
<span v-if="isBroadcasting">Broadcasting...</span>
|
||||
<span v-else>Broadcast to Nostr</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Your profile is automatically broadcast to Nostr when you update it or log in.
|
||||
Use the "Broadcast to Nostr" button to manually re-broadcast your profile.
|
||||
Your profile is broadcast to Nostr automatically when you save changes.
|
||||
</p>
|
||||
</form>
|
||||
|
||||
|
|
@ -189,7 +174,7 @@ import * as z from 'zod'
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { User, Zap, Hash, Radio } from 'lucide-vue-next'
|
||||
import { User, Zap, Hash } from 'lucide-vue-next'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
|
|
@ -215,19 +200,16 @@ 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 imageService = injectService<ImageUploadService>(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
||||
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
||||
const toast = useToast()
|
||||
|
||||
// Local state
|
||||
const isUpdating = ref(false)
|
||||
const isBroadcasting = ref(false)
|
||||
const updateError = ref<string | null>(null)
|
||||
const updateSuccess = ref(false)
|
||||
const uploadedPicture = ref<any[]>([])
|
||||
|
|
@ -323,18 +305,12 @@ const updateUserProfile = async (formData: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Update profile via AuthService (which updates LNbits)
|
||||
// Update profile via AuthService (which updates LNbits).
|
||||
// Kind-0 metadata publishing happens server-side as part of the
|
||||
// PATCH /api/v1/auth handler (aiolabs/lnbits 869f67c3).
|
||||
await updateProfile(updateData)
|
||||
|
||||
// Broadcast to Nostr automatically
|
||||
try {
|
||||
await metadataService.publishMetadata()
|
||||
toast.success('Profile updated and broadcast to Nostr!')
|
||||
} catch (nostrError) {
|
||||
console.error('Failed to broadcast to Nostr:', nostrError)
|
||||
toast.warning('Profile updated, but failed to broadcast to Nostr')
|
||||
}
|
||||
|
||||
toast.success('Profile updated!')
|
||||
updateSuccess.value = true
|
||||
|
||||
// Clear success message after 3 seconds
|
||||
|
|
@ -352,22 +328,6 @@ const updateUserProfile = async (formData: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Manually broadcast metadata to Nostr
|
||||
const broadcastMetadata = async () => {
|
||||
isBroadcasting.value = true
|
||||
|
||||
try {
|
||||
const result = await metadataService.publishMetadata()
|
||||
toast.success(`Profile broadcast to ${result.success}/${result.total} relays!`)
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Failed to broadcast metadata'
|
||||
console.error('Error broadcasting metadata:', error)
|
||||
toast.error(`Failed to broadcast: ${errorMessage}`)
|
||||
} finally {
|
||||
isBroadcasting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Log out + redirect to /login on this app's origin.
|
||||
const onLogout = async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import type { NostrMetadataService, NostrMetadata } from '../nostr/nostr-metadata-service'
|
||||
|
||||
/**
|
||||
* Composable for accessing Nostr metadata service
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const { publishMetadata, getMetadata } = useNostrMetadata()
|
||||
*
|
||||
* // Get current metadata
|
||||
* const metadata = getMetadata()
|
||||
*
|
||||
* // Publish metadata to Nostr relays
|
||||
* await publishMetadata()
|
||||
* ```
|
||||
*/
|
||||
export function useNostrMetadata() {
|
||||
const metadataService = injectService<NostrMetadataService>(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
||||
|
||||
/**
|
||||
* Publish user metadata to Nostr relays (NIP-01 kind 0)
|
||||
*/
|
||||
const publishMetadata = async (): Promise<{ success: number; total: number }> => {
|
||||
return await metadataService.publishMetadata()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user's Nostr metadata
|
||||
*/
|
||||
const getMetadata = (): NostrMetadata => {
|
||||
return metadataService.getMetadata()
|
||||
}
|
||||
|
||||
return {
|
||||
publishMetadata,
|
||||
getMetadata
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ import type { App } from 'vue'
|
|||
import type { ModulePlugin } from '@/core/types'
|
||||
import { container, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import { relayHub } from './nostr/relay-hub'
|
||||
import { NostrMetadataService } from './nostr/nostr-metadata-service'
|
||||
import { ProfileService } from './nostr/ProfileService'
|
||||
import { ReactionService } from './nostr/ReactionService'
|
||||
import { NostrTransportService } from './services/NostrTransportService'
|
||||
|
|
@ -30,7 +29,6 @@ import ProfileSettings from './components/ProfileSettings.vue'
|
|||
const invoiceService = new InvoiceService()
|
||||
const lnbitsAPI = new LnbitsAPI()
|
||||
const imageUploadService = new ImageUploadService()
|
||||
const nostrMetadataService = new NostrMetadataService()
|
||||
const profileService = new ProfileService()
|
||||
const reactionService = new ReactionService()
|
||||
const nostrTransportService = new NostrTransportService()
|
||||
|
|
@ -48,7 +46,6 @@ export const baseModule: ModulePlugin = {
|
|||
|
||||
// Register core Nostr services
|
||||
container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
|
||||
container.provide(SERVICE_TOKENS.NOSTR_METADATA_SERVICE, nostrMetadataService)
|
||||
|
||||
// Register auth service
|
||||
container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)
|
||||
|
|
@ -113,10 +110,6 @@ export const baseModule: ModulePlugin = {
|
|||
waitForDependencies: true, // ImageUploadService depends on ToastService
|
||||
maxRetries: 3
|
||||
})
|
||||
await nostrMetadataService.initialize({
|
||||
waitForDependencies: true, // NostrMetadataService depends on AuthService and RelayHub
|
||||
maxRetries: 3
|
||||
})
|
||||
await profileService.initialize({
|
||||
waitForDependencies: true, // ProfileService depends on RelayHub
|
||||
maxRetries: 3
|
||||
|
|
@ -145,7 +138,6 @@ export const baseModule: ModulePlugin = {
|
|||
await storageService.dispose()
|
||||
await toastService.dispose()
|
||||
await imageUploadService.dispose()
|
||||
await nostrMetadataService.dispose()
|
||||
await profileService.dispose()
|
||||
await reactionService.dispose()
|
||||
await nostrTransportService.dispose()
|
||||
|
|
@ -156,7 +148,6 @@ export const baseModule: ModulePlugin = {
|
|||
container.remove(SERVICE_TOKENS.LNBITS_API)
|
||||
container.remove(SERVICE_TOKENS.INVOICE_SERVICE)
|
||||
container.remove(SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE)
|
||||
container.remove(SERVICE_TOKENS.NOSTR_METADATA_SERVICE)
|
||||
container.remove(SERVICE_TOKENS.PROFILE_SERVICE)
|
||||
container.remove(SERVICE_TOKENS.REACTION_SERVICE)
|
||||
|
||||
|
|
@ -173,7 +164,6 @@ export const baseModule: ModulePlugin = {
|
|||
invoiceService,
|
||||
pwaService,
|
||||
imageUploadService,
|
||||
nostrMetadataService,
|
||||
profileService,
|
||||
reactionService
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,162 +0,0 @@
|
|||
import { BaseService } from '@/core/base/BaseService'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import { finalizeEvent, type EventTemplate } from 'nostr-tools'
|
||||
import type { AuthService } from '@/modules/base/auth/auth-service'
|
||||
import type { RelayHub } from '@/modules/base/nostr/relay-hub'
|
||||
|
||||
/**
|
||||
* Nostr User Metadata (NIP-01 kind 0)
|
||||
* https://github.com/nostr-protocol/nips/blob/master/01.md
|
||||
*/
|
||||
export interface NostrMetadata {
|
||||
name?: string // Display name (from username)
|
||||
display_name?: string // Alternative display name
|
||||
about?: string // Bio/description
|
||||
picture?: string // Profile picture URL
|
||||
banner?: string // Profile banner URL
|
||||
nip05?: string // NIP-05 identifier (username@domain)
|
||||
lud16?: string // Lightning Address (same as nip05)
|
||||
website?: string // Personal website
|
||||
}
|
||||
|
||||
/**
|
||||
* Service for publishing and managing Nostr user metadata (NIP-01 kind 0)
|
||||
*
|
||||
* This service handles:
|
||||
* - Publishing user profile metadata to Nostr relays
|
||||
* - Syncing LNbits user data with Nostr profile
|
||||
* - Auto-broadcasting metadata on login and profile updates
|
||||
*/
|
||||
export class NostrMetadataService extends BaseService {
|
||||
protected readonly metadata = {
|
||||
name: 'NostrMetadataService',
|
||||
version: '1.0.0',
|
||||
dependencies: ['AuthService', 'RelayHub']
|
||||
}
|
||||
|
||||
protected authService: AuthService | null = null
|
||||
protected relayHub: RelayHub | null = null
|
||||
|
||||
protected async onInitialize(): Promise<void> {
|
||||
console.log('NostrMetadataService: Starting initialization...')
|
||||
|
||||
this.authService = injectService<AuthService>(SERVICE_TOKENS.AUTH_SERVICE)
|
||||
this.relayHub = injectService<RelayHub>(SERVICE_TOKENS.RELAY_HUB)
|
||||
|
||||
if (!this.authService) {
|
||||
throw new Error('AuthService not available')
|
||||
}
|
||||
|
||||
if (!this.relayHub) {
|
||||
throw new Error('RelayHub service not available')
|
||||
}
|
||||
|
||||
console.log('NostrMetadataService: Initialization complete')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Nostr metadata from LNbits user data
|
||||
*/
|
||||
private buildMetadata(): NostrMetadata {
|
||||
const user = this.authService?.user.value
|
||||
if (!user) {
|
||||
throw new Error('No authenticated user')
|
||||
}
|
||||
|
||||
const lightningDomain = import.meta.env.VITE_LIGHTNING_DOMAIN || window.location.hostname
|
||||
const username = user.username || user.id.slice(0, 8)
|
||||
|
||||
const metadata: NostrMetadata = {
|
||||
name: username,
|
||||
nip05: `${username}@${lightningDomain}`,
|
||||
lud16: `${username}@${lightningDomain}`
|
||||
}
|
||||
|
||||
// Add optional fields from user.extra if they exist
|
||||
if (user.extra?.display_name) {
|
||||
metadata.display_name = user.extra.display_name
|
||||
}
|
||||
|
||||
if (user.extra?.picture) {
|
||||
metadata.picture = user.extra.picture
|
||||
}
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish user metadata to Nostr relays (NIP-01 kind 0)
|
||||
*
|
||||
* This creates a replaceable event that updates the user's profile.
|
||||
* Only the latest kind 0 event for a given pubkey is kept by relays.
|
||||
*/
|
||||
async publishMetadata(): Promise<{ success: number; total: number }> {
|
||||
if (!this.authService?.isAuthenticated.value) {
|
||||
throw new Error('Must be authenticated to publish metadata')
|
||||
}
|
||||
|
||||
if (!this.relayHub?.isConnected.value) {
|
||||
throw new Error('Not connected to relays')
|
||||
}
|
||||
|
||||
const user = this.authService.user.value
|
||||
if (!user?.prvkey) {
|
||||
throw new Error('User private key not available')
|
||||
}
|
||||
|
||||
try {
|
||||
const metadata = this.buildMetadata()
|
||||
|
||||
console.log('📤 Publishing Nostr metadata (kind 0):', metadata)
|
||||
|
||||
// Create kind 0 event (user metadata)
|
||||
// Content is JSON-stringified metadata
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 0,
|
||||
content: JSON.stringify(metadata),
|
||||
tags: [],
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
// Sign the event
|
||||
const privkeyBytes = this.hexToUint8Array(user.prvkey)
|
||||
const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
|
||||
|
||||
console.log('✅ Metadata event signed:', signedEvent.id)
|
||||
console.log('📋 Full signed event:', JSON.stringify(signedEvent, null, 2))
|
||||
|
||||
// Publish to all connected relays
|
||||
const result = await this.relayHub.publishEvent(signedEvent)
|
||||
|
||||
console.log(`✅ Metadata published to ${result.success}/${result.total} relays`)
|
||||
|
||||
return result
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to publish metadata:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current user's Nostr metadata
|
||||
*/
|
||||
getMetadata(): NostrMetadata {
|
||||
return this.buildMetadata()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert hex string to Uint8Array
|
||||
*/
|
||||
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
|
||||
}
|
||||
|
||||
protected async onDestroy(): Promise<void> {
|
||||
// Cleanup if needed
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue