- Revised .env.portainer to update sensitive credentials and streamline comments for clarity. - Adjusted docker-compose.yml to remove unnecessary variable references, enhancing readability and maintainability. - Updated VideoPlayer component to improve type handling and refactor seeking logic for better performance. - Enhanced library service to include providerId in the rentContent method for improved data handling. - Refactored auth store to integrate account management functionality. - Cleaned up ProjectEditor and Settings views by removing unused computed properties and refining method types.
466 lines
13 KiB
TypeScript
466 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)
|
|
return {
|
|
id: mockUserId,
|
|
email: `${pubkey.slice(0, 8)}@nostr.local`,
|
|
legalName: 'Nostr User',
|
|
nostrPubkey: pubkey,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
filmmaker: {
|
|
id: 'mock-filmmaker-' + pubkey.slice(0, 8),
|
|
userId: mockUserId,
|
|
professionalName: 'Nostr Filmmaker',
|
|
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
|
|
*/
|
|
async function loginWithNostr(pubkey: string, signature: string, event: any) {
|
|
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)
|
|
const response = await authService.createNostrSession({
|
|
pubkey,
|
|
signature,
|
|
event,
|
|
})
|
|
|
|
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,
|
|
}
|
|
})
|