Files
indee-demo/src/stores/auth.ts
Dorian 11d289d793 feat: add comment support for Lightning payments in BTCPay and Strike services
- 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.
2026-02-14 13:02:42 +00:00

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,
}
})