Compare commits
2 commits
be182cff3f
...
85fc83cb42
| Author | SHA1 | Date | |
|---|---|---|---|
| 85fc83cb42 | |||
| 531de18ad7 |
6 changed files with 74 additions and 91 deletions
|
|
@ -40,8 +40,12 @@ interface User {
|
||||||
username?: string
|
username?: string
|
||||||
email?: string
|
email?: string
|
||||||
pubkey?: string
|
pubkey?: string
|
||||||
// pragma: allowlist secret
|
// The `prvkey` field was removed from this interface as the final step of
|
||||||
prvkey?: string // Nostr signing key for user
|
// phase-1 per aiolabs/lnbits#9 / design-questions Q1.2 Option (b). LNbits
|
||||||
|
// signs server-side via the NostrSigner abstraction (PR #26) and exposes
|
||||||
|
// `signer_type` instead of raw key material on /api/v1/auth. Bucket-B
|
||||||
|
// sign-sites (kind 1 / 4 / 5 / 7 / 31925 / 10003 / 1111 etc.) migrate to
|
||||||
|
// POST /api/v1/auth/sign-event (PR #29) in phase 2.
|
||||||
external_id?: string
|
external_id?: string
|
||||||
extensions: string[]
|
extensions: string[]
|
||||||
wallets: Wallet[]
|
wallets: Wallet[]
|
||||||
|
|
@ -158,19 +162,21 @@ export class LnbitsAPI extends BaseService {
|
||||||
// First get basic user info from /auth
|
// First get basic user info from /auth
|
||||||
const basicUser = await this.request<User>('/auth')
|
const basicUser = await this.request<User>('/auth')
|
||||||
|
|
||||||
// Then get Nostr keys from /auth/nostr/me (this was working in main branch)
|
// /auth/nostr/me used to return the user's prvkey for client-side signing;
|
||||||
|
// post-aiolabs/lnbits#9 phase-1 the server signs and the endpoint returns
|
||||||
|
// only the pubkey. We keep the call to merge the pubkey (which the basic
|
||||||
|
// /auth response also includes on the post-cascade server; this is the
|
||||||
|
// belt-and-suspenders fallback for older lnbits revisions until we ship a
|
||||||
|
// signer_type-aware client).
|
||||||
try {
|
try {
|
||||||
const nostrUser = await this.request<User>('/auth/nostr/me')
|
const nostrUser = await this.request<User>('/auth/nostr/me')
|
||||||
|
|
||||||
// Merge the data - basic user info + Nostr keys
|
|
||||||
return {
|
return {
|
||||||
...basicUser,
|
...basicUser,
|
||||||
pubkey: nostrUser.pubkey,
|
pubkey: nostrUser.pubkey,
|
||||||
prvkey: nostrUser.prvkey
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to fetch Nostr keys, returning basic user info:', error)
|
console.warn('Failed to fetch Nostr pubkey from /auth/nostr/me, returning basic user info:', error)
|
||||||
// Return basic user info without Nostr keys if the endpoint fails
|
|
||||||
return basicUser
|
return basicUser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ import { Button } from '@/components/ui/button'
|
||||||
import { CalendarPlus } from 'lucide-vue-next'
|
import { CalendarPlus } from 'lucide-vue-next'
|
||||||
import { useAuth } from '@/composables/useAuthService'
|
import { useAuth } from '@/composables/useAuthService'
|
||||||
import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import type { ActivitiesNostrService } from '../services/ActivitiesNostrService'
|
import type { TicketApiService } from '../services/TicketApiService'
|
||||||
import type { CalendarTimeEvent } from '../types/nip52'
|
import type { CreateEventRequest } from '../types/ticket'
|
||||||
import type { ActivityCategory } from '../types/category'
|
import type { ActivityCategory } from '../types/category'
|
||||||
import CategorySelector from './CategorySelector.vue'
|
import CategorySelector from './CategorySelector.vue'
|
||||||
import LocationPicker from './LocationPicker.vue'
|
import LocationPicker from './LocationPicker.vue'
|
||||||
|
|
@ -67,56 +67,64 @@ const form = useForm({
|
||||||
const isFormValid = computed(() => form.meta.value.valid)
|
const isFormValid = computed(() => form.meta.value.valid)
|
||||||
|
|
||||||
const onSubmit = form.handleSubmit(async (values) => {
|
const onSubmit = form.handleSubmit(async (values) => {
|
||||||
const nostrService = tryInjectService<ActivitiesNostrService>(SERVICE_TOKENS.ACTIVITIES_NOSTR_SERVICE)
|
const ticketApi = tryInjectService<TicketApiService>(SERVICE_TOKENS.TICKET_API)
|
||||||
if (!nostrService) {
|
if (!ticketApi) {
|
||||||
toast.error('Activities service not available')
|
toast.error('Activities service not available')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const signingKey = currentUser.value?.prvkey
|
const invoiceKey = currentUser.value?.wallets?.[0]?.inkey
|
||||||
if (!signingKey) {
|
if (!invoiceKey) {
|
||||||
toast.error('Signing key not available. Please log in again.')
|
toast.error('No wallet available. Please log in first.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
isPublishing.value = true
|
isPublishing.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build unix timestamps
|
// Compose ISO 8601 datetime strings the events extension parses.
|
||||||
const startTimestamp = Math.floor(new Date(`${values.startDate}T${values.startTime}`).getTime() / 1000)
|
const startIso = `${values.startDate}T${values.startTime}`
|
||||||
let endTimestamp: number | undefined
|
const endIso =
|
||||||
if (values.endDate && values.endTime) {
|
values.endDate && values.endTime
|
||||||
endTimestamp = Math.floor(new Date(`${values.endDate}T${values.endTime}`).getTime() / 1000)
|
? `${values.endDate}T${values.endTime}`
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
// Fold summary + description into `info` since the events extension
|
||||||
|
// CreateEventRequest has no separate summary field.
|
||||||
|
const info =
|
||||||
|
values.summary && values.description
|
||||||
|
? `${values.summary}\n\n${values.description}`
|
||||||
|
: values.description || values.summary || ''
|
||||||
|
|
||||||
|
// Ticket-less activity — amount_tickets and price_per_ticket both
|
||||||
|
// pinned at 0 (events extension treats 0 as "unlimited / not
|
||||||
|
// ticketed" per models.py:45-46). Server-side `signer.sign_event`
|
||||||
|
// produces the kind-31922 calendar event and publishes via the
|
||||||
|
// operator's configured relays — no webapp signing path needed.
|
||||||
|
const eventData: CreateEventRequest = {
|
||||||
|
name: values.title,
|
||||||
|
info,
|
||||||
|
event_start_date: startIso,
|
||||||
|
event_end_date: endIso,
|
||||||
|
location: location.value || null,
|
||||||
|
banner: values.image || null,
|
||||||
|
categories: selectedCategories.value,
|
||||||
|
amount_tickets: 0,
|
||||||
|
price_per_ticket: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a unique d-tag
|
await ticketApi.createEvent(eventData, invoiceKey)
|
||||||
const dTag = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
|
||||||
|
|
||||||
const eventData: Partial<CalendarTimeEvent> = {
|
// Approval workflow caveat: non-admin users on instances with
|
||||||
dTag,
|
// `auto_approve=false` (the default) land in the proposal queue;
|
||||||
title: values.title,
|
// their event isn't published to relays until an admin approves.
|
||||||
summary: values.summary || undefined,
|
// Admins-and-auto-approve-on instances publish immediately.
|
||||||
content: values.description,
|
toast.success('Activity created!')
|
||||||
image: values.image || undefined,
|
emit('created')
|
||||||
start: startTimestamp,
|
handleClose()
|
||||||
end: endTimestamp,
|
|
||||||
startTzid: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
||||||
location: location.value || undefined,
|
|
||||||
hashtags: selectedCategories.value,
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await nostrService.publishCalendarEvent(eventData, signingKey)
|
|
||||||
|
|
||||||
if (result.success > 0) {
|
|
||||||
toast.success(`Activity published to ${result.success} relay${result.success > 1 ? 's' : ''}`)
|
|
||||||
emit('created')
|
|
||||||
handleClose()
|
|
||||||
} else {
|
|
||||||
toast.error('Failed to publish to any relay')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to publish activity:', err)
|
console.error('Failed to create activity:', err)
|
||||||
toast.error(err instanceof Error ? err.message : 'Failed to publish activity')
|
toast.error(err instanceof Error ? err.message : 'Failed to create activity')
|
||||||
} finally {
|
} finally {
|
||||||
isPublishing.value = false
|
isPublishing.value = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import { BaseService } from '@/core/base/BaseService'
|
import { BaseService } from '@/core/base/BaseService'
|
||||||
import { finalizeEvent, type Event as NostrEvent, type EventTemplate } from 'nostr-tools'
|
import type { Event as NostrEvent } from 'nostr-tools'
|
||||||
import type { SubscriptionConfig } from '@/modules/base/nostr/relay-hub'
|
import type { SubscriptionConfig } from '@/modules/base/nostr/relay-hub'
|
||||||
import {
|
import {
|
||||||
NIP52_KINDS,
|
NIP52_KINDS,
|
||||||
parseCalendarTimeEvent,
|
parseCalendarTimeEvent,
|
||||||
parseCalendarDateEvent,
|
parseCalendarDateEvent,
|
||||||
buildCalendarTimeEventTags,
|
|
||||||
type CalendarTimeEvent,
|
|
||||||
} from '../types/nip52'
|
} from '../types/nip52'
|
||||||
import {
|
import {
|
||||||
calendarTimeEventToActivity,
|
calendarTimeEventToActivity,
|
||||||
|
|
@ -28,7 +26,15 @@ export interface CalendarEventFilters {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service for subscribing to and publishing NIP-52 Calendar Events via RelayHub.
|
* Service for subscribing to NIP-52 Calendar Events via RelayHub.
|
||||||
|
*
|
||||||
|
* Publishing kind-31922 calendar events lives server-side in the
|
||||||
|
* `aiolabs/events` LNbits extension (signer-abstraction branch, commit
|
||||||
|
* 66076d6) — `POST /events/api/v1/events` constructs and signs the
|
||||||
|
* event via NostrSigner and broadcasts it to the operator's configured
|
||||||
|
* relays. The webapp constructs only the request payload; see
|
||||||
|
* CreateActivityDialog for the flow.
|
||||||
|
*
|
||||||
* Extends BaseService for standardized dependency injection and lifecycle.
|
* Extends BaseService for standardized dependency injection and lifecycle.
|
||||||
*/
|
*/
|
||||||
export class ActivitiesNostrService extends BaseService {
|
export class ActivitiesNostrService extends BaseService {
|
||||||
|
|
@ -105,32 +111,6 @@ export class ActivitiesNostrService extends BaseService {
|
||||||
return activities
|
return activities
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Publish a NIP-52 time-based calendar event.
|
|
||||||
* Requires an authenticated user with a signing key.
|
|
||||||
*/
|
|
||||||
async publishCalendarEvent(
|
|
||||||
eventData: Partial<CalendarTimeEvent>,
|
|
||||||
signingKeyHex: string
|
|
||||||
): Promise<{ success: number; total: number }> {
|
|
||||||
if (!this.relayHub) {
|
|
||||||
throw new Error('RelayHub not available')
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = buildCalendarTimeEventTags(eventData)
|
|
||||||
const template: EventTemplate = {
|
|
||||||
kind: NIP52_KINDS.CALENDAR_TIME_EVENT,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
content: eventData.content ?? '',
|
|
||||||
tags,
|
|
||||||
}
|
|
||||||
|
|
||||||
const privkeyBytes = hexToUint8Array(signingKeyHex)
|
|
||||||
const signedEvent = finalizeEvent(template, privkeyBytes)
|
|
||||||
|
|
||||||
return await this.relayHub.publishEvent(signedEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a raw Nostr event into an Activity view model.
|
* Parse a raw Nostr event into an Activity view model.
|
||||||
*/
|
*/
|
||||||
|
|
@ -179,11 +159,3 @@ export class ActivitiesNostrService extends BaseService {
|
||||||
this.activeUnsubscribes = []
|
this.activeUnsubscribes = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function 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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -180,17 +180,14 @@ export class AuthService extends BaseService {
|
||||||
this.isLoading.value = true
|
this.isLoading.value = true
|
||||||
const updatedUser = await this.lnbitsAPI.updateProfile(data)
|
const updatedUser = await this.lnbitsAPI.updateProfile(data)
|
||||||
|
|
||||||
// Preserve prvkey and pubkey from existing user since /auth/update doesn't return them
|
// Preserve pubkey from existing user since /auth/update doesn't return it.
|
||||||
|
// Kind-0 metadata is published server-side by lnbits's PATCH /auth handler
|
||||||
|
// (aiolabs/lnbits commit 869f67c3); no webapp-side broadcast path remains.
|
||||||
this.user.value = {
|
this.user.value = {
|
||||||
...updatedUser,
|
...updatedUser,
|
||||||
pubkey: this.user.value?.pubkey || updatedUser.pubkey,
|
pubkey: this.user.value?.pubkey || updatedUser.pubkey,
|
||||||
prvkey: this.user.value?.prvkey || updatedUser.prvkey
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
} catch (error) {
|
||||||
const err = this.handleError(error, 'updateProfile')
|
const err = this.handleError(error, 'updateProfile')
|
||||||
throw err
|
throw err
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ import ThreadedPost from './ThreadedPost.vue'
|
||||||
import ScheduledEventCard from './ScheduledEventCard.vue'
|
import ScheduledEventCard from './ScheduledEventCard.vue'
|
||||||
import appConfig from '@/app.config'
|
import appConfig from '@/app.config'
|
||||||
import type { ContentFilter, FeedPost } from '../services/FeedService'
|
import type { ContentFilter, FeedPost } from '../services/FeedService'
|
||||||
import type { ScheduledEvent } from '../services/ScheduledEventService'
|
import type { ScheduledEvent } from '@/modules/tasks/services/TaskService'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import type { AuthService } from '@/modules/base/auth/auth-service'
|
import type { AuthService } from '@/modules/base/auth/auth-service'
|
||||||
import type { RelayHub } from '@/modules/base/nostr/relay-hub'
|
import type { RelayHub } from '@/modules/base/nostr/relay-hub'
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from '@/components/ui/collapsible'
|
} from '@/components/ui/collapsible'
|
||||||
import { Calendar, MapPin, Clock, CheckCircle, PlayCircle, Hand, Trash2 } from 'lucide-vue-next'
|
import { Calendar, MapPin, Clock, CheckCircle, PlayCircle, Hand, Trash2 } from 'lucide-vue-next'
|
||||||
import type { ScheduledEvent, EventCompletion, TaskStatus } from '../services/ScheduledEventService'
|
import type { ScheduledEvent, EventCompletion, TaskStatus } from '@/modules/tasks/services/TaskService'
|
||||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import type { AuthService } from '@/modules/base/auth/auth-service'
|
import type { AuthService } from '@/modules/base/auth/auth-service'
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue