- Enhanced the sendPaymentWithAddress method in BTCPayService and StrikeService to accept an optional comment parameter. - Updated resolveLightningAddress to include the comment in the callback URL if supported by the LNURL-pay endpoint. - Modified PaymentService to construct a descriptive comment for Lightning invoices, improving clarity for users. These changes enhance the payment experience by allowing users to include contextual information with their transactions.
477 lines
13 KiB
TypeScript
477 lines
13 KiB
TypeScript
import { defineStore } from 'pinia'
|
|
import { ref } from 'vue'
|
|
import { authService } from '../services/auth.service'
|
|
import { nip98Service } from '../services/nip98.service'
|
|
import { accountManager } from '../lib/accounts'
|
|
import type { ApiUser } from '../types/api'
|
|
import { USE_MOCK } from '../utils/mock'
|
|
|
|
/** Returns true when the error looks like a network / connection failure */
|
|
function isConnectionError(error: any): boolean {
|
|
const msg = error?.message?.toLowerCase() || ''
|
|
return (
|
|
msg.includes('unable to connect') ||
|
|
msg.includes('network error') ||
|
|
msg.includes('failed to fetch') ||
|
|
msg.includes('econnrefused')
|
|
)
|
|
}
|
|
|
|
/** Build a mock Nostr user with filmmaker profile + subscription */
|
|
function buildMockNostrUser(pubkey: string) {
|
|
const mockUserId = 'mock-nostr-user-' + pubkey.slice(0, 8)
|
|
const shortPub = pubkey.slice(0, 8)
|
|
return {
|
|
id: mockUserId,
|
|
email: `${shortPub}@nostr.local`,
|
|
legalName: `npub...${pubkey.slice(-6)}`,
|
|
nostrPubkey: pubkey,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
filmmaker: {
|
|
id: 'mock-filmmaker-' + shortPub,
|
|
userId: mockUserId,
|
|
professionalName: `npub...${pubkey.slice(-6)}`,
|
|
bio: 'Independent filmmaker and content creator.',
|
|
},
|
|
subscriptions: [{
|
|
id: 'mock-sub-cinephile',
|
|
userId: mockUserId,
|
|
tier: 'cinephile' as const,
|
|
status: 'active' as const,
|
|
currentPeriodStart: new Date().toISOString(),
|
|
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
cancelAtPeriodEnd: false,
|
|
stripePriceId: 'mock-price-cinephile',
|
|
stripeCustomerId: 'mock-customer-' + pubkey.slice(0, 8),
|
|
}],
|
|
}
|
|
}
|
|
|
|
/** Build a mock Cognito user with subscription */
|
|
function buildMockCognitoUser(email: string, legalName?: string) {
|
|
const username = email.split('@')[0]
|
|
return {
|
|
id: 'mock-user-' + username,
|
|
email,
|
|
legalName: legalName || username.charAt(0).toUpperCase() + username.slice(1),
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
subscriptions: [{
|
|
id: 'mock-sub-cinephile',
|
|
userId: 'mock-user-' + username,
|
|
tier: 'cinephile' as const,
|
|
status: 'active' as const,
|
|
currentPeriodStart: new Date().toISOString(),
|
|
currentPeriodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
cancelAtPeriodEnd: false,
|
|
stripePriceId: 'mock-price-cinephile',
|
|
stripeCustomerId: 'mock-customer-' + username,
|
|
}],
|
|
}
|
|
}
|
|
|
|
export type AuthType = 'cognito' | 'nostr' | null
|
|
|
|
export interface AuthState {
|
|
user: ApiUser | null
|
|
authType: AuthType
|
|
isAuthenticated: boolean
|
|
nostrPubkey: string | null
|
|
cognitoToken: string | null
|
|
isLoading: boolean
|
|
}
|
|
|
|
/**
|
|
* Authentication Store
|
|
* Manages user authentication state with dual Cognito/Nostr support
|
|
*/
|
|
export const useAuthStore = defineStore('auth', () => {
|
|
// State
|
|
const user = ref<ApiUser | null>(null)
|
|
const authType = ref<AuthType>(null)
|
|
const isAuthenticated = ref(false)
|
|
const nostrPubkey = ref<string | null>(null)
|
|
const cognitoToken = ref<string | null>(null)
|
|
const isLoading = ref(false)
|
|
|
|
/**
|
|
* Initialize auth state from stored tokens.
|
|
* In dev/mock mode, reconstructs the mock user directly from
|
|
* sessionStorage rather than calling the (non-existent) backend API.
|
|
*/
|
|
async function initialize() {
|
|
// Bail out early if already authenticated — prevents
|
|
// re-initialization from wiping state on subsequent navigations
|
|
if (isAuthenticated.value && user.value) {
|
|
return
|
|
}
|
|
|
|
isLoading.value = true
|
|
|
|
try {
|
|
const storedCognitoToken = sessionStorage.getItem('auth_token')
|
|
const storedNostrToken = sessionStorage.getItem('nostr_token')
|
|
const storedPubkey = sessionStorage.getItem('nostr_pubkey')
|
|
|
|
if (!storedCognitoToken && !storedNostrToken) {
|
|
return // Nothing stored — not logged in
|
|
}
|
|
|
|
// Helper: restore mock session from sessionStorage
|
|
const restoreAsMock = () => {
|
|
if (storedNostrToken && storedPubkey) {
|
|
user.value = buildMockNostrUser(storedPubkey)
|
|
nostrPubkey.value = storedPubkey
|
|
authType.value = 'nostr'
|
|
isAuthenticated.value = true
|
|
} else if (storedCognitoToken) {
|
|
user.value = buildMockCognitoUser('dev@local', 'Dev User')
|
|
cognitoToken.value = storedCognitoToken
|
|
authType.value = 'cognito'
|
|
isAuthenticated.value = true
|
|
}
|
|
}
|
|
|
|
if (USE_MOCK) {
|
|
restoreAsMock()
|
|
return
|
|
}
|
|
|
|
// Real mode: validate session with backend API.
|
|
// For Nostr sessions, skip the Cognito-only validate-session endpoint
|
|
// and go straight to /auth/me which uses HybridAuthGuard.
|
|
try {
|
|
if (storedNostrToken && storedPubkey) {
|
|
// Nostr session: restore nip98Service state then fetch user profile
|
|
nip98Service.storeTokens(
|
|
storedNostrToken,
|
|
sessionStorage.getItem('indeehub_api_refresh') ?? sessionStorage.getItem('refresh_token') ?? '',
|
|
)
|
|
|
|
await fetchCurrentUser()
|
|
nostrPubkey.value = storedPubkey
|
|
authType.value = 'nostr'
|
|
isAuthenticated.value = true
|
|
} else if (storedCognitoToken) {
|
|
// Cognito session: use legacy validate-session
|
|
const isValid = await authService.validateSession()
|
|
if (isValid) {
|
|
await fetchCurrentUser()
|
|
authType.value = 'cognito'
|
|
cognitoToken.value = storedCognitoToken
|
|
isAuthenticated.value = true
|
|
} else {
|
|
await logout()
|
|
}
|
|
}
|
|
} catch (apiError: any) {
|
|
if (isConnectionError(apiError)) {
|
|
console.warn('Backend not reachable — falling back to mock session.')
|
|
restoreAsMock()
|
|
} else {
|
|
// Token likely expired or invalid
|
|
console.warn('Session validation failed:', apiError.message)
|
|
if (accountManager.active) {
|
|
// Still have a Nostr signer — try re-authenticating
|
|
restoreAsMock()
|
|
} else {
|
|
await logout()
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to initialize auth:', error)
|
|
await logout()
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Login with email and password (Cognito)
|
|
*/
|
|
async function loginWithCognito(email: string, password: string) {
|
|
isLoading.value = true
|
|
|
|
// Mock Cognito login helper
|
|
const mockLogin = () => {
|
|
const mockUser = buildMockCognitoUser(email)
|
|
console.log('🔧 Using mock Cognito authentication')
|
|
|
|
cognitoToken.value = 'mock-jwt-token-' + Date.now()
|
|
authType.value = 'cognito'
|
|
user.value = mockUser
|
|
isAuthenticated.value = true
|
|
|
|
sessionStorage.setItem('auth_token', cognitoToken.value)
|
|
sessionStorage.setItem('refresh_token', 'mock-refresh-token')
|
|
|
|
return {
|
|
accessToken: cognitoToken.value,
|
|
idToken: 'mock-id-token',
|
|
refreshToken: 'mock-refresh-token',
|
|
expiresIn: 3600,
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (USE_MOCK) {
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
return mockLogin()
|
|
}
|
|
|
|
// Real API call
|
|
const response = await authService.login({ email, password })
|
|
|
|
cognitoToken.value = response.accessToken
|
|
authType.value = 'cognito'
|
|
|
|
await fetchCurrentUser()
|
|
|
|
isAuthenticated.value = true
|
|
|
|
return response
|
|
} catch (error: any) {
|
|
if (isConnectionError(error)) {
|
|
console.warn('Backend not reachable — falling back to mock Cognito login.')
|
|
return mockLogin()
|
|
}
|
|
throw error
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Login with Nostr signature.
|
|
*
|
|
* @param preSignedAuthHeader Optional pre-built `Nostr <base64>` header.
|
|
* When provided the active signer is NOT called for NIP-98 signing.
|
|
* This is required for Amber / clipboard-based signers that cannot
|
|
* sign events inline.
|
|
*/
|
|
async function loginWithNostr(
|
|
pubkey: string,
|
|
signature: string,
|
|
event: any,
|
|
preSignedAuthHeader?: string,
|
|
) {
|
|
isLoading.value = true
|
|
|
|
// Mock Nostr login helper
|
|
const mockLogin = () => {
|
|
const mockUser = buildMockNostrUser(pubkey)
|
|
console.warn('🔧 Using mock Nostr authentication (backend not available)')
|
|
|
|
nostrPubkey.value = pubkey
|
|
authType.value = 'nostr'
|
|
user.value = mockUser
|
|
isAuthenticated.value = true
|
|
|
|
sessionStorage.setItem('nostr_token', 'mock-nostr-token-' + pubkey.slice(0, 16))
|
|
sessionStorage.setItem('nostr_pubkey', pubkey)
|
|
|
|
return { token: 'mock-nostr-token', user: mockUser }
|
|
}
|
|
|
|
try {
|
|
if (USE_MOCK) {
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
return mockLogin()
|
|
}
|
|
|
|
// Real API call — creates NIP-98 signed session via the active
|
|
// Nostr account in accountManager (set by useAccounts before this call).
|
|
// When preSignedAuthHeader is provided the signer is bypassed.
|
|
const response = await authService.createNostrSession(
|
|
{ pubkey, signature, event },
|
|
preSignedAuthHeader,
|
|
)
|
|
|
|
nostrPubkey.value = pubkey
|
|
authType.value = 'nostr'
|
|
sessionStorage.setItem('nostr_pubkey', pubkey)
|
|
isAuthenticated.value = true
|
|
|
|
// Backend returns JWT tokens but no user object.
|
|
// Fetch the user profile with the new access token.
|
|
try {
|
|
await fetchCurrentUser()
|
|
} catch {
|
|
// User may not exist in DB yet — create a minimal local representation
|
|
user.value = buildMockNostrUser(pubkey)
|
|
}
|
|
|
|
return response
|
|
} catch (error: any) {
|
|
if (isConnectionError(error)) {
|
|
console.warn('Backend not reachable — falling back to mock Nostr login.')
|
|
return mockLogin()
|
|
}
|
|
throw error
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register new user
|
|
*/
|
|
async function register(email: string, password: string, legalName: string) {
|
|
isLoading.value = true
|
|
|
|
// Mock registration helper
|
|
const mockRegister = () => {
|
|
const mockUser = buildMockCognitoUser(email, legalName)
|
|
console.warn('🔧 Using mock registration (backend not available)')
|
|
|
|
cognitoToken.value = 'mock-jwt-token-' + Date.now()
|
|
authType.value = 'cognito'
|
|
user.value = mockUser
|
|
isAuthenticated.value = true
|
|
|
|
sessionStorage.setItem('auth_token', cognitoToken.value)
|
|
sessionStorage.setItem('refresh_token', 'mock-refresh-token')
|
|
|
|
return {
|
|
accessToken: cognitoToken.value,
|
|
idToken: 'mock-id-token',
|
|
refreshToken: 'mock-refresh-token',
|
|
expiresIn: 3600,
|
|
}
|
|
}
|
|
|
|
try {
|
|
if (USE_MOCK) {
|
|
await new Promise(resolve => setTimeout(resolve, 500))
|
|
return mockRegister()
|
|
}
|
|
|
|
// Real API call
|
|
const response = await authService.register({
|
|
email,
|
|
password,
|
|
legalName,
|
|
})
|
|
|
|
cognitoToken.value = response.accessToken
|
|
authType.value = 'cognito'
|
|
|
|
await fetchCurrentUser()
|
|
|
|
isAuthenticated.value = true
|
|
|
|
return response
|
|
} catch (error: any) {
|
|
if (isConnectionError(error)) {
|
|
console.warn('Backend not reachable — falling back to mock registration.')
|
|
return mockRegister()
|
|
}
|
|
throw error
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Fetch current user data
|
|
*/
|
|
async function fetchCurrentUser() {
|
|
try {
|
|
const userData = await authService.getCurrentUser()
|
|
user.value = userData
|
|
|
|
if (userData.nostrPubkey) {
|
|
nostrPubkey.value = userData.nostrPubkey
|
|
}
|
|
|
|
return userData
|
|
} catch (error) {
|
|
console.error('Failed to fetch user:', error)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logout user
|
|
*/
|
|
async function logout() {
|
|
await authService.logout()
|
|
nip98Service.clearSession()
|
|
|
|
user.value = null
|
|
authType.value = null
|
|
isAuthenticated.value = false
|
|
nostrPubkey.value = null
|
|
cognitoToken.value = null
|
|
}
|
|
|
|
/**
|
|
* Link Nostr pubkey to account
|
|
*/
|
|
async function linkNostr(pubkey: string, signature: string) {
|
|
try {
|
|
const updatedUser = await authService.linkNostrPubkey(pubkey, signature)
|
|
user.value = updatedUser
|
|
nostrPubkey.value = pubkey
|
|
return updatedUser
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unlink Nostr pubkey from account
|
|
*/
|
|
async function unlinkNostr() {
|
|
try {
|
|
const updatedUser = await authService.unlinkNostrPubkey()
|
|
user.value = updatedUser
|
|
nostrPubkey.value = null
|
|
return updatedUser
|
|
} catch (error) {
|
|
throw error
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if user is filmmaker
|
|
*/
|
|
function isFilmmaker(): boolean {
|
|
return !!user.value?.filmmaker
|
|
}
|
|
|
|
/**
|
|
* Check if user has active subscription
|
|
*/
|
|
function hasActiveSubscription(): boolean {
|
|
if (!user.value?.subscriptions) return false
|
|
return user.value.subscriptions.some((sub) => sub.status === 'active')
|
|
}
|
|
|
|
return {
|
|
// State
|
|
user,
|
|
authType,
|
|
isAuthenticated,
|
|
nostrPubkey,
|
|
cognitoToken,
|
|
isLoading,
|
|
|
|
// Actions
|
|
initialize,
|
|
loginWithCognito,
|
|
loginWithNostr,
|
|
register,
|
|
fetchCurrentUser,
|
|
logout,
|
|
linkNostr,
|
|
unlinkNostr,
|
|
|
|
// Getters
|
|
isFilmmaker,
|
|
hasActiveSubscription,
|
|
}
|
|
})
|