Enhance deployment script and update package dependencies

- Added detailed labels to the deployment script for IndeedHub, including title, version, description, license, icon, and repository URL.
- Updated package dependencies in package.json and package-lock.json, including upgrading 'nostr-tools' to version 2.23.0 and adding 'axios' and '@tanstack/vue-query'.
- Improved README with a modern description of the platform and updated project structure details.

This commit enhances the clarity of the deployment process and ensures the project is using the latest dependencies for better performance and features.
This commit is contained in:
Dorian
2026-02-12 10:30:47 +00:00
parent dacfa7a822
commit c970f5b29f
43 changed files with 6906 additions and 603 deletions

267
src/services/api.service.ts Normal file
View File

@@ -0,0 +1,267 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosError } from 'axios'
import { apiConfig } from '../config/api.config'
import type { ApiError } from '../types/api'
/**
* Base API Service
* Handles HTTP requests, token management, and error handling
*/
class ApiService {
private client: AxiosInstance
private tokenRefreshPromise: Promise<string> | null = null
constructor() {
this.client = axios.create({
baseURL: apiConfig.baseURL,
timeout: apiConfig.timeout,
headers: {
'Content-Type': 'application/json',
},
})
this.setupInterceptors()
}
/**
* Setup request and response interceptors
*/
private setupInterceptors() {
// Request interceptor - Add auth token
this.client.interceptors.request.use(
(config) => {
const token = this.getToken()
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => Promise.reject(error)
)
// Response interceptor - Handle errors and token refresh
this.client.interceptors.response.use(
(response) => response,
async (error: AxiosError<ApiError>) => {
const originalRequest = error.config as AxiosRequestConfig & { _retry?: boolean }
// Handle 401 - Token expired
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
try {
const newToken = await this.refreshToken()
if (newToken && originalRequest.headers) {
originalRequest.headers.Authorization = `Bearer ${newToken}`
return this.client(originalRequest)
}
} catch (refreshError) {
// Token refresh failed - clear auth and redirect to login
this.clearAuth()
window.location.href = '/login'
return Promise.reject(refreshError)
}
}
// Handle other errors
return Promise.reject(this.handleError(error))
}
)
}
/**
* Get stored authentication token
*/
private getToken(): string | null {
// Check session storage first (Cognito JWT)
const cognitoToken = sessionStorage.getItem('auth_token')
if (cognitoToken) return cognitoToken
// Check for Nostr session token
const nostrToken = sessionStorage.getItem('nostr_token')
if (nostrToken) return nostrToken
return null
}
/**
* Set authentication token
*/
public setToken(token: string, type: 'cognito' | 'nostr' = 'cognito') {
if (type === 'cognito') {
sessionStorage.setItem('auth_token', token)
} else {
sessionStorage.setItem('nostr_token', token)
}
}
/**
* Clear authentication
*/
public clearAuth() {
sessionStorage.removeItem('auth_token')
sessionStorage.removeItem('nostr_token')
sessionStorage.removeItem('refresh_token')
}
/**
* Refresh authentication token
*/
private async refreshToken(): Promise<string> {
// Prevent multiple simultaneous refresh requests
if (this.tokenRefreshPromise) {
return this.tokenRefreshPromise
}
this.tokenRefreshPromise = (async () => {
try {
const refreshToken = sessionStorage.getItem('refresh_token')
if (!refreshToken) {
throw new Error('No refresh token available')
}
// Call refresh endpoint (implement based on backend)
const response = await axios.post(`${apiConfig.baseURL}/auth/refresh`, {
refreshToken,
})
const newToken = response.data.accessToken
this.setToken(newToken, 'cognito')
if (response.data.refreshToken) {
sessionStorage.setItem('refresh_token', response.data.refreshToken)
}
return newToken
} finally {
this.tokenRefreshPromise = null
}
})()
return this.tokenRefreshPromise
}
/**
* Handle and normalize API errors
*/
private handleError(error: AxiosError<ApiError>): ApiError {
if (error.response) {
// Server responded with error
return {
message: error.response.data?.message || 'An error occurred',
statusCode: error.response.status,
error: error.response.data?.error,
details: error.response.data?.details,
}
} else if (error.request) {
// Request made but no response
return {
message: 'Unable to connect to server. Please check your internet connection.',
statusCode: 0,
error: 'NETWORK_ERROR',
}
} else {
// Something else happened
return {
message: error.message || 'An unexpected error occurred',
statusCode: 0,
error: 'UNKNOWN_ERROR',
}
}
}
/**
* Retry logic for failed requests
*/
private async retryRequest<T>(
fn: () => Promise<T>,
retries: number = apiConfig.maxRetries
): Promise<T> {
try {
return await fn()
} catch (error) {
if (retries > 0 && this.shouldRetry(error as AxiosError)) {
await this.delay(apiConfig.retryDelay)
return this.retryRequest(fn, retries - 1)
}
throw error
}
}
/**
* Determine if request should be retried
*/
private shouldRetry(error: AxiosError): boolean {
if (!apiConfig.enableRetry) return false
// Retry on network errors or 5xx server errors
return (
!error.response ||
(error.response.status >= 500 && error.response.status < 600)
)
}
/**
* Delay helper for retry logic
*/
private delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
}
/**
* GET request
*/
public async get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
if (apiConfig.enableRetry) {
return this.retryRequest(async () => {
const response = await this.client.get<T>(url, config)
return response.data
})
}
const response = await this.client.get<T>(url, config)
return response.data
}
/**
* POST request
*/
public async post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post<T>(url, data, config)
return response.data
}
/**
* PUT request
*/
public async put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.put<T>(url, data, config)
return response.data
}
/**
* PATCH request
*/
public async patch<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.patch<T>(url, data, config)
return response.data
}
/**
* DELETE request
*/
public async delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.delete<T>(url, config)
return response.data
}
/**
* Get CDN URL for media assets
*/
public getCdnUrl(path: string): string {
if (!path) return ''
if (path.startsWith('http')) return path
return `${apiConfig.cdnURL}${path}`
}
}
// Export singleton instance
export const apiService = new ApiService()

View File

@@ -0,0 +1,147 @@
import { apiService } from './api.service'
import type {
LoginCredentials,
RegisterData,
AuthResponse,
NostrSessionRequest,
NostrSessionResponse,
ApiUser,
} from '../types/api'
/**
* Authentication Service
* Handles Cognito and Nostr authentication
*/
class AuthService {
/**
* Login with email and password (Cognito)
*/
async login(credentials: LoginCredentials): Promise<AuthResponse> {
try {
const response = await apiService.post<AuthResponse>('/auth/login', credentials)
// Store tokens
if (response.accessToken) {
apiService.setToken(response.accessToken, 'cognito')
if (response.refreshToken) {
sessionStorage.setItem('refresh_token', response.refreshToken)
}
}
return response
} catch (error) {
throw error
}
}
/**
* Register new user
*/
async register(data: RegisterData): Promise<AuthResponse> {
return apiService.post<AuthResponse>('/auth/register', data)
}
/**
* Get current authenticated user
*/
async getCurrentUser(): Promise<ApiUser> {
return apiService.get<ApiUser>('/auth/me')
}
/**
* Validate current session
*/
async validateSession(): Promise<boolean> {
try {
await apiService.post('/auth/validate-session')
return true
} catch {
return false
}
}
/**
* Logout user
*/
async logout(): Promise<void> {
apiService.clearAuth()
}
/**
* Create Nostr session
*/
async createNostrSession(request: NostrSessionRequest): Promise<NostrSessionResponse> {
const response = await apiService.post<NostrSessionResponse>('/auth/nostr/session', request)
// Store Nostr token
if (response.token) {
apiService.setToken(response.token, 'nostr')
}
return response
}
/**
* Refresh Nostr session
*/
async refreshNostrSession(pubkey: string, signature: string): Promise<NostrSessionResponse> {
return apiService.post<NostrSessionResponse>('/auth/nostr/refresh', {
pubkey,
signature,
})
}
/**
* Link Nostr pubkey to existing account
*/
async linkNostrPubkey(pubkey: string, signature: string): Promise<ApiUser> {
return apiService.post<ApiUser>('/auth/nostr/link', {
pubkey,
signature,
})
}
/**
* Unlink Nostr pubkey from account
*/
async unlinkNostrPubkey(): Promise<ApiUser> {
return apiService.post<ApiUser>('/auth/nostr/unlink')
}
/**
* Initialize OTP flow
*/
async initOtp(email: string): Promise<void> {
await apiService.post('/auth/otp/init', { email })
}
/**
* Request password reset
*/
async forgotPassword(email: string): Promise<void> {
await apiService.post('/auth/forgot-password', { email })
}
/**
* Reset password with code
*/
async resetPassword(email: string, code: string, newPassword: string): Promise<void> {
await apiService.post('/auth/reset-password', {
email,
code,
newPassword,
})
}
/**
* Confirm email with verification code
*/
async confirmEmail(email: string, code: string): Promise<void> {
await apiService.post('/auth/confirm-email', {
email,
code,
})
}
}
export const authService = new AuthService()

View File

@@ -0,0 +1,111 @@
import { apiService } from './api.service'
import type { ApiProject, ApiContent } from '../types/api'
/**
* Content Service
* Handles projects and content data
*/
class ContentService {
/**
* Get all published projects with optional filters
*/
async getProjects(filters?: {
type?: 'film' | 'episodic' | 'music-video'
status?: string
genre?: string
limit?: number
page?: number
}): Promise<ApiProject[]> {
const params = new URLSearchParams()
if (filters?.type) params.append('type', filters.type)
if (filters?.status) params.append('status', filters.status)
if (filters?.genre) params.append('genre', filters.genre)
if (filters?.limit) params.append('limit', filters.limit.toString())
if (filters?.page) params.append('page', filters.page.toString())
const url = `/projects${params.toString() ? `?${params.toString()}` : ''}`
return apiService.get<ApiProject[]>(url)
}
/**
* Get project by ID
*/
async getProjectById(id: string): Promise<ApiProject> {
return apiService.get<ApiProject>(`/projects/${id}`)
}
/**
* Get project by slug
*/
async getProjectBySlug(slug: string): Promise<ApiProject> {
return apiService.get<ApiProject>(`/projects/slug/${slug}`)
}
/**
* Get content by ID
*/
async getContentById(id: string): Promise<ApiContent> {
return apiService.get<ApiContent>(`/contents/${id}`)
}
/**
* Get all contents for a project
*/
async getContentsByProject(projectId: string): Promise<ApiContent[]> {
return apiService.get<ApiContent[]>(`/contents/project/${projectId}`)
}
/**
* Get streaming URL for content (requires subscription or rental)
*/
async getStreamingUrl(contentId: string): Promise<{ url: string; drmToken?: string }> {
return apiService.get<{ url: string; drmToken?: string }>(`/contents/${contentId}/stream`)
}
/**
* Search projects
*/
async searchProjects(query: string, filters?: {
type?: string
genre?: string
}): Promise<ApiProject[]> {
const params = new URLSearchParams()
params.append('q', query)
if (filters?.type) params.append('type', filters.type)
if (filters?.genre) params.append('genre', filters.genre)
return apiService.get<ApiProject[]>(`/projects/search?${params.toString()}`)
}
/**
* Get featured content (top-rated, recent releases)
*/
async getFeaturedContent(): Promise<ApiProject[]> {
return apiService.get<ApiProject[]>('/projects?status=published&featured=true')
}
/**
* Get genres
*/
async getGenres(): Promise<Array<{ id: string; name: string; slug: string }>> {
return apiService.get('/genres')
}
/**
* Get festivals
*/
async getFestivals(): Promise<Array<{ id: string; name: string; slug: string }>> {
return apiService.get('/festivals')
}
/**
* Get awards
*/
async getAwards(): Promise<Array<{ id: string; name: string; slug: string }>> {
return apiService.get('/awards')
}
}
export const contentService = new ContentService()

View File

@@ -0,0 +1,145 @@
import { apiService } from './api.service'
import type { ApiRent, ApiContent } from '../types/api'
/**
* Library Service
* Handles user library and rentals
*/
class LibraryService {
/**
* Get user's library (subscribed + rented content)
*/
async getUserLibrary(): Promise<{
subscribed: ApiContent[]
rented: ApiRent[]
continueWatching: Array<{ content: ApiContent; progress: number }>
}> {
// Check if we're in development mode
const useMockData = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
if (useMockData) {
// Mock library data for development
console.log('🔧 Development mode: Using mock library data')
await new Promise(resolve => setTimeout(resolve, 300))
// Import mock film data
const { indeeHubFilms, bitcoinFilms } = await import('../data/indeeHubFilms')
const allFilms = [...indeeHubFilms, ...bitcoinFilms]
// Create mock API content from our film data
const mockApiContent = allFilms.slice(0, 20).map((film) => ({
id: film.id,
projectId: film.id,
title: film.title,
synopsis: film.description,
file: `/content/${film.id}/video.mp4`,
status: 'ready' as const,
rentalPrice: 4.99,
poster: film.thumbnail,
metadata: { duration: film.duration },
isRssEnabled: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}))
// Mock continue watching (first 3 films with progress)
const continueWatching = mockApiContent.slice(0, 3).map((content, index) => ({
content,
progress: [35, 67, 12][index], // Different progress percentages
}))
// Mock rented content (2 films with expiry)
const rented: ApiRent[] = mockApiContent.slice(3, 5).map((content) => ({
id: 'rent-' + content.id,
userId: 'mock-user',
contentId: content.id,
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24 hours
createdAt: new Date().toISOString(),
content,
}))
// All subscribed content (full catalog access)
const subscribed = mockApiContent
return {
subscribed,
rented,
continueWatching,
}
}
// Real API call
return apiService.get('/library')
}
/**
* Get rented content
*/
async getRentedContent(): Promise<ApiRent[]> {
return apiService.get<ApiRent[]>('/rents')
}
/**
* Rent content
*/
async rentContent(contentId: string, paymentMethodId?: string): Promise<ApiRent> {
return apiService.post<ApiRent>('/rents', {
contentId,
paymentMethodId,
})
}
/**
* Check if user has access to content
*/
async checkContentAccess(contentId: string): Promise<{
hasAccess: boolean
method?: 'subscription' | 'rental'
expiresAt?: string
}> {
try {
return await apiService.get(`/contents/${contentId}/access`)
} catch {
return { hasAccess: false }
}
}
/**
* Add content to watch later list
*/
async addToWatchLater(contentId: string): Promise<void> {
await apiService.post('/library/watch-later', { contentId })
}
/**
* Remove content from watch later list
*/
async removeFromWatchLater(contentId: string): Promise<void> {
await apiService.delete(`/library/watch-later/${contentId}`)
}
/**
* Update watch progress
*/
async updateWatchProgress(contentId: string, progress: number, duration: number): Promise<void> {
await apiService.post('/library/progress', {
contentId,
progress,
duration,
})
}
/**
* Get watch progress for content
*/
async getWatchProgress(contentId: string): Promise<{ progress: number; duration: number } | null> {
try {
return await apiService.get(`/library/progress/${contentId}`)
} catch {
return null
}
}
}
export const libraryService = new LibraryService()

View File

@@ -0,0 +1,114 @@
import { apiService } from './api.service'
import type { ApiSubscription } from '../types/api'
/**
* Subscription Service
* Handles user subscriptions
*/
class SubscriptionService {
/**
* Get user's subscriptions
*/
async getSubscriptions(): Promise<ApiSubscription[]> {
return apiService.get<ApiSubscription[]>('/subscriptions')
}
/**
* Get active subscription
*/
async getActiveSubscription(): Promise<ApiSubscription | null> {
const subscriptions = await this.getSubscriptions()
return subscriptions.find((sub) => sub.status === 'active') || null
}
/**
* Subscribe to a tier
*/
async subscribe(data: {
tier: 'enthusiast' | 'film-buff' | 'cinephile'
period: 'monthly' | 'annual'
paymentMethodId?: string
}): Promise<ApiSubscription> {
return apiService.post<ApiSubscription>('/subscriptions', data)
}
/**
* Cancel subscription
*/
async cancelSubscription(subscriptionId: string): Promise<void> {
await apiService.delete(`/subscriptions/${subscriptionId}`)
}
/**
* Resume cancelled subscription
*/
async resumeSubscription(subscriptionId: string): Promise<ApiSubscription> {
return apiService.post<ApiSubscription>(`/subscriptions/${subscriptionId}/resume`)
}
/**
* Update payment method
*/
async updatePaymentMethod(subscriptionId: string, paymentMethodId: string): Promise<void> {
await apiService.patch(`/subscriptions/${subscriptionId}/payment-method`, {
paymentMethodId,
})
}
/**
* Get subscription tiers with pricing
*/
async getSubscriptionTiers(): Promise<Array<{
tier: string
name: string
monthlyPrice: number
annualPrice: number
features: string[]
}>> {
// This might be a static endpoint or hardcoded
// Adjust based on actual API
return [
{
tier: 'enthusiast',
name: 'Enthusiast',
monthlyPrice: 9.99,
annualPrice: 99.99,
features: [
'Access to all films and series',
'HD streaming',
'Watch on 2 devices',
'Cancel anytime',
],
},
{
tier: 'film-buff',
name: 'Film Buff',
monthlyPrice: 19.99,
annualPrice: 199.99,
features: [
'Everything in Enthusiast',
'4K streaming',
'Watch on 4 devices',
'Exclusive behind-the-scenes content',
'Early access to new releases',
],
},
{
tier: 'cinephile',
name: 'Cinephile',
monthlyPrice: 29.99,
annualPrice: 299.99,
features: [
'Everything in Film Buff',
'Watch on unlimited devices',
'Offline downloads',
'Director commentary tracks',
'Virtual festival access',
'Support independent filmmakers',
],
},
]
}
}
export const subscriptionService = new SubscriptionService()