Implement backend API and database services in Docker setup

- Added a new `api` service for the NestJS backend, including health checks and dependencies on PostgreSQL, Redis, and MinIO.
- Introduced PostgreSQL and Redis services with health checks and configurations for data persistence.
- Added MinIO for S3-compatible object storage and a one-shot service to initialize required buckets.
- Updated the Nginx configuration to proxy requests to the new backend API and MinIO storage.
- Enhanced the Dockerfile to support the new API environment variables and configurations.
- Updated the `package.json` and `package-lock.json` to include new dependencies for QR code generation and other utilities.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Dorian
2026-02-12 20:14:39 +00:00
parent f19fd6feef
commit cdd24a5def
478 changed files with 55355 additions and 529 deletions

View File

@@ -6,8 +6,9 @@ import { topDocFilms, topDocBitcoin, topDocCrypto, topDocMoney, topDocEconomics
import { contentService } from '../services/content.service'
import { mapApiProjectsToContents } from '../utils/mappers'
import { useContentSourceStore } from './contentSource'
const USE_MOCK_DATA = import.meta.env.VITE_USE_MOCK_DATA === 'true' || import.meta.env.DEV
import { indeehubApiService } from '../services/indeehub-api.service'
import { useFilmmaker } from '../composables/useFilmmaker'
import { USE_MOCK as USE_MOCK_DATA } from '../utils/mock'
export const useContentStore = defineStore('content', () => {
const featuredContent = ref<Content | null>(null)
@@ -24,25 +25,21 @@ export const useContentStore = defineStore('content', () => {
const error = ref<string | null>(null)
/**
* Fetch content from API
* Fetch content from the original API (external IndeeHub)
*/
async function fetchContentFromApi() {
try {
// Fetch all published projects
const projects = await contentService.getProjects({ status: 'published' })
if (projects.length === 0) {
throw new Error('No content available')
}
// Map API data to content format
const allContent = mapApiProjectsToContents(projects)
// Set featured content (first film project or first project)
const featuredFilm = allContent.find(c => c.type === 'film') || allContent[0]
featuredContent.value = featuredFilm
// Organize into rows
const films = allContent.filter(c => c.type === 'film')
const bitcoinContent = allContent.filter(c =>
c.categories?.some(cat => cat.toLowerCase().includes('bitcoin'))
@@ -68,6 +65,64 @@ export const useContentStore = defineStore('content', () => {
}
}
/**
* Fetch content from our self-hosted IndeeHub API
*/
async function fetchContentFromIndeehubApi() {
try {
const response = await indeehubApiService.getProjects()
// Handle both array responses and wrapped responses like { data: [...] }
const projects = Array.isArray(response) ? response : (response as any)?.data ?? []
if (!Array.isArray(projects) || projects.length === 0) {
throw new Error('No content available from IndeeHub API')
}
// Map API projects to frontend Content format
const allContent: Content[] = projects.map((p: any) => ({
id: p.id,
title: p.title,
description: p.synopsis || '',
thumbnail: p.poster || '',
backdrop: p.poster || '',
type: p.type || 'film',
slug: p.slug,
rentalPrice: p.rentalPrice,
status: p.status,
categories: p.genre ? [p.genre.name] : [],
streamingUrl: p.streamingUrl || p.partnerStreamUrl || undefined,
apiData: {
deliveryMode: p.deliveryMode,
partnerStreamUrl: p.partnerStreamUrl,
partnerDashUrl: p.partnerDashUrl,
partnerFairplayUrl: p.partnerFairplayUrl,
partnerDrmToken: p.partnerDrmToken,
},
}))
const featuredFilm = allContent.find(c => c.type === 'film') || allContent[0]
featuredContent.value = featuredFilm
const films = allContent.filter(c => c.type === 'film')
const bitcoinContent = allContent.filter(c =>
c.categories?.some(cat => cat.toLowerCase().includes('bitcoin') || cat.toLowerCase().includes('documentary'))
)
contentRows.value = {
featured: allContent.slice(0, 10),
newReleases: films.slice(0, 8),
bitcoin: bitcoinContent.length > 0 ? bitcoinContent : films.slice(0, 6),
documentaries: allContent.slice(0, 10),
dramas: films.slice(0, 6),
independent: films.slice(0, 10)
}
} catch (err) {
console.error('IndeeHub API fetch failed:', err)
throw err
}
}
/**
* Fetch IndeeHub mock content (original catalog)
*/
@@ -111,7 +166,52 @@ export const useContentStore = defineStore('content', () => {
}
/**
* Route to the correct mock loader based on the active content source
* Convert published filmmaker projects to Content format and merge
* them into the existing content rows so they appear on the browse page.
*/
function mergePublishedFilmmakerProjects() {
try {
const { projects } = useFilmmaker()
const published = projects.value.filter(p => p.status === 'published')
if (published.length === 0) return
const publishedContent: Content[] = published.map(p => ({
id: p.id,
title: p.title || p.name,
description: p.synopsis || '',
thumbnail: p.poster || '/images/placeholder-poster.jpg',
backdrop: p.poster || '/images/placeholder-poster.jpg',
type: p.type === 'episodic' ? 'series' as const : 'film' as const,
rating: p.format || undefined,
releaseYear: p.releaseDate ? new Date(p.releaseDate).getFullYear() : new Date().getFullYear(),
categories: p.genres?.map(g => g.name) || [],
slug: p.slug,
rentalPrice: p.rentalPrice,
status: p.status,
apiData: p,
}))
// Merge into each content row (prepend so they appear first)
for (const key of Object.keys(contentRows.value)) {
// Avoid duplicates by filtering out any already-present IDs
const existingIds = new Set(contentRows.value[key].map(c => c.id))
const newItems = publishedContent.filter(c => !existingIds.has(c.id))
if (newItems.length > 0) {
contentRows.value[key] = [...newItems, ...contentRows.value[key]]
}
}
// If no featured content yet, use the first published project
if (!featuredContent.value && publishedContent.length > 0) {
featuredContent.value = publishedContent[0]
}
} catch {
// Filmmaker composable may not be initialized yet — safe to ignore
}
}
/**
* Route to the correct loader based on the active content source
*/
function fetchContentFromMock() {
const sourceStore = useContentSourceStore()
@@ -120,6 +220,9 @@ export const useContentStore = defineStore('content', () => {
} else {
fetchIndeeHubMock()
}
// In mock mode, also include any projects published through the backstage
mergePublishedFilmmakerProjects()
}
/**
@@ -130,12 +233,17 @@ export const useContentStore = defineStore('content', () => {
error.value = null
try {
if (USE_MOCK_DATA) {
const sourceStore = useContentSourceStore()
if (sourceStore.activeSource === 'indeehub-api' && !USE_MOCK_DATA) {
// Fetch from our self-hosted backend (only when backend is actually running)
await fetchContentFromIndeehubApi()
} else if (USE_MOCK_DATA) {
// Use mock data in development or when flag is set
await new Promise(resolve => setTimeout(resolve, 100))
fetchContentFromMock()
} else {
// Fetch from API
// Fetch from original API
await fetchContentFromApi()
}
} catch (e: any) {