From 3ca43b62e4407bd13496372c4df1a6516ea1ad4f Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 13 Feb 2026 12:35:03 +0000 Subject: [PATCH] Enhance Docker and backend configurations for improved deployment - Updated docker-compose.yml to include environment variable support for services, enhancing flexibility in configuration. - Refactored Dockerfile to utilize build arguments for VITE environment variables, allowing for better customization during builds. - Improved Nginx configuration to handle larger video uploads by increasing client_max_body_size to 5GB. - Enhanced backend Dockerfile to include wget for health checks and improved startup logging for database migrations. - Added validation for critical environment variables in the backend to ensure necessary configurations are present before application startup. - Updated content streaming logic to support direct HLS URL construction, improving streaming reliability and user experience. - Refactored various components and services to streamline access checks and improve error handling during content playback. --- .env.portainer | 107 +++++++++ Dockerfile | 26 +- backend/.dockerignore | 13 + backend/Dockerfile | 10 +- backend/src/auth/auth.module.ts | 3 + .../auth/guards/optional-hybrid-auth.guard.ts | 30 +++ backend/src/common/validate-env.ts | 73 ++++++ backend/src/contents/contents.controller.ts | 30 ++- backend/src/main.ts | 38 ++- .../projects/dto/response/base-project.dto.ts | 25 +- backend/src/projects/projects.service.ts | 5 +- backend/src/scripts/seed-content.ts | 116 +-------- docker-compose.dev.yml | 186 +++++++++++++++ docker-compose.yml | 224 ++++++++++++------ nginx.conf | 4 +- scripts/dev.sh | 6 +- src/components/ContentDetailModal.vue | 19 +- src/components/VideoPlayer.vue | 50 +++- src/composables/useAccess.ts | 18 +- src/services/library.service.ts | 6 +- src/services/nip98.service.ts | 10 +- src/stores/content.ts | 41 +++- src/types/content.ts | 3 + 23 files changed, 799 insertions(+), 244 deletions(-) create mode 100644 .env.portainer create mode 100644 backend/.dockerignore create mode 100644 backend/src/auth/guards/optional-hybrid-auth.guard.ts create mode 100644 backend/src/common/validate-env.ts create mode 100644 docker-compose.dev.yml diff --git a/.env.portainer b/.env.portainer new file mode 100644 index 0000000..ad83129 --- /dev/null +++ b/.env.portainer @@ -0,0 +1,107 @@ +# ═══════════════════════════════════════════════════════════════ +# IndeeHub — Portainer Stack Environment Variables +# ═══════════════════════════════════════════════════════════════ +# +# Upload this file in Portainer: Stacks → Add Stack → "Load variables +# from .env file" button (bottom of the environment variables section). +# +# Secrets below were auto-generated. Review and change DOMAIN / +# FRONTEND_URL to match your server, then deploy. +# +# For local dev: docker compose -f docker-compose.dev.yml up +# ═══════════════════════════════════════════════════════════════ + +# ── Networking ──────────────────────────────────────────────── +# CHANGE THESE to your actual domain +DOMAIN=indee.tx1138.com +FRONTEND_URL=https://indee.tx1138.com +APP_PORT=7777 + +# ── PostgreSQL ──────────────────────────────────────────────── +POSTGRES_USER=indeedhub +POSTGRES_PASSWORD=V1kKEPzvKuFqENfQQQKGMnXSgzS3HCY3 +POSTGRES_DB=indeedhub + +# ── Redis ───────────────────────────────────────────────────── +REDIS_PASSWORD=wWNOWmlYPKrFDMSWj1g7aEpX + +# ── MinIO (self-hosted file storage) ────────────────────────── +# MinIO runs as a Docker container in the stack. No AWS needed. +# It stores uploaded videos, poster images, and HLS segments. +MINIO_ROOT_USER=indeedhub-minio +MINIO_ROOT_PASSWORD=jZWDalDHlaJ9JmywL05h8alf +MINIO_CONSOLE_PORT=9001 + +# ── MinIO Connection (backend uses S3 protocol to talk to MinIO) +# These MUST match the MinIO credentials above. +S3_ENDPOINT=http://minio:9000 +AWS_REGION=us-east-1 +S3_ACCESS_KEY=indeedhub-minio +S3_SECRET_KEY=jZWDalDHlaJ9JmywL05h8alf +S3_PRIVATE_BUCKET=indeedhub-private +S3_PUBLIC_BUCKET=indeedhub-public +# CHANGE to your domain — files are served via the nginx /storage/ proxy +S3_PUBLIC_BUCKET_URL=https://indee.tx1138.com/storage/ + +# ── CloudFront (not needed — MinIO serves files directly) ──── +CLOUDFRONT_PRIVATE_KEY= +CLOUDFRONT_KEY_PAIR_ID= +CLOUDFRONT_DISTRIBUTION_URL= + +# ── BTCPay Server (Bitcoin/Lightning Payments) ─────────────── +BTCPAY_URL=https://shop.tx1138.com +BTCPAY_API_KEY=34cf1a41aba967f24f374c247fdb67e81fc303ff +BTCPAY_STORE_ID=72jsCiD7m2U8dDJRU5W5STBSrRScLbxfiSaPBhLXukem +BTCPAY_WEBHOOK_SECRET=36YdN12LB9vhKAi9bhbWe5wT4oa3 +BTCPAY_ROUTE_HINTS=false + +# ── Security Secrets (auto-generated) ──────────────────────── +NOSTR_JWT_SECRET=9ea6d10ae3b74cfe90f5d5dc0494fe20bbc06e0f2cef43649cced6ce8fbaf66d +NOSTR_JWT_EXPIRES_IN=7d +AES_MASTER_SECRET=639a0a363d9ab6d31407ba4ec874b8ab + +# ── SMTP / Email (leave empty to disable) ──────────────────── +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASS= +MAIL_FROM=noreply@tx1138.com + +# ── SendGrid (alternative to SMTP, leave empty if not using) ─ +SENDGRID_API_KEY= +SENDGRID_SENDER= + +# ── Cognito (not needed — using Nostr auth) ────────────────── +COGNITO_USER_POOL_ID= +COGNITO_CLIENT_ID= + +# ── Flash Subscription Secrets (leave empty if not using) ──── +FLASH_JWT_SECRET_ENTHUSIAST= +FLASH_JWT_SECRET_FILM_BUFF= +FLASH_JWT_SECRET_CINEPHILE= +FLASH_JWT_SECRET_RSS_ADDON= +FLASH_JWT_SECRET_VERIFICATION_ADDON= + +# ── Transcoding API (leave empty — uses built-in FFmpeg) ───── +TRANSCODING_API_KEY= +TRANSCODING_API_URL= + +# ── Analytics & Monitoring (leave empty to disable) ────────── +POSTHOG_API_KEY= +SENTRY_ENVIRONMENT=production + +# ── DRM (not needed — using AES-128 HLS encryption) ───────── +DRM_SECRET_NAME= +PRIVATE_AUTH_CERTIFICATE_KEY_ID= + +# ── Podping (leave empty to disable) ───────────────────────── +PODPING_URL= +PODPING_KEY= +PODPING_USER_AGENT= + +# ── Admin Dashboard ─────────────────────────────────────────── +ADMIN_API_KEY=c45fc7deda784dafadf0ce6b98d808e49b84892a + +# ── Partner Content (leave empty if not using) ──────────────── +PARTNER_API_BASE_URL= +PARTNER_API_KEY= diff --git a/Dockerfile b/Dockerfile index eb0aead..86b9ce0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,20 +12,20 @@ RUN npm ci # Copy source code COPY . . -# Clear VITE_NOSTR_RELAYS so the app auto-detects the relay -# via the /relay nginx proxy at runtime (instead of hardcoding localhost) -ENV VITE_NOSTR_RELAYS="" +# ── Build-time configuration via ARGs ──────────────────────── +# These are baked into the static JS bundle at build time. +# Override them with docker-compose build.args or --build-arg. +ARG VITE_NOSTR_RELAYS="" +ARG VITE_USE_MOCK_DATA=false +ARG VITE_CONTENT_ORIGIN= +ARG VITE_INDEEHUB_API_URL=/api +ARG VITE_INDEEHUB_CDN_URL=/storage -# Enable mock data mode as default — set to false to use the backend API -ENV VITE_USE_MOCK_DATA=true - -# Content origin must match the seeder's ORIGIN so that relay queries find -# the seeded data, regardless of how users access the app in their browser -ENV VITE_CONTENT_ORIGIN=http://localhost:7777 - -# IndeeHub self-hosted backend API (via nginx /api proxy) -ENV VITE_INDEEHUB_API_URL=/api -ENV VITE_INDEEHUB_CDN_URL=/storage +ENV VITE_NOSTR_RELAYS=${VITE_NOSTR_RELAYS} +ENV VITE_USE_MOCK_DATA=${VITE_USE_MOCK_DATA} +ENV VITE_CONTENT_ORIGIN=${VITE_CONTENT_ORIGIN} +ENV VITE_INDEEHUB_API_URL=${VITE_INDEEHUB_API_URL} +ENV VITE_INDEEHUB_CDN_URL=${VITE_INDEEHUB_CDN_URL} # Build the application RUN npm run build diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..2b4924e --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,13 @@ +node_modules +dist +.git +.env +.env.* +npm-debug.log* +.DS_Store +.vscode +.idea +.cursor +*.swp +*.swo +*~ diff --git a/backend/Dockerfile b/backend/Dockerfile index 6120552..d22a9f8 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -14,6 +14,9 @@ RUN npm prune --production FROM node:20-alpine AS production WORKDIR /app +# wget is needed for Docker/Portainer health checks +RUN apk add --no-cache wget + COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package-lock.json ./package-lock.json COPY --from=builder /app/dist ./dist @@ -21,5 +24,8 @@ COPY --from=builder /app/node_modules ./node_modules EXPOSE 4000 -# Run TypeORM migrations on startup, then start the API -CMD ["sh", "-c", "npx typeorm migration:run -d dist/database/ormconfig.js 2>/dev/null; export NODE_OPTIONS='--max-old-space-size=1024' && npm run start:prod"] +ENV NODE_OPTIONS="--max-old-space-size=1024" + +# Run TypeORM migrations on startup, then start the API. +# Migration errors are logged (not suppressed) so failed deploys are visible. +CMD ["sh", "-c", "echo 'Running database migrations...' && npx typeorm migration:run -d dist/database/ormconfig.js && echo 'Migrations complete.' && npm run start:prod"] diff --git a/backend/src/auth/auth.module.ts b/backend/src/auth/auth.module.ts index 6c62d3f..faf8354 100644 --- a/backend/src/auth/auth.module.ts +++ b/backend/src/auth/auth.module.ts @@ -10,6 +10,7 @@ import { NostrAuthModule } from 'src/nostr-auth/nostr-auth.module'; import { JwtAuthGuard } from './guards/jwt.guard'; import { TokenAuthGuard } from './guards/token.guard'; import { HybridAuthGuard } from './guards/hybrid-auth.guard'; +import { OptionalHybridAuthGuard } from './guards/optional-hybrid-auth.guard'; import { NostrSessionService } from './nostr-session.service'; import { NostrSessionJwtGuard } from './guards/nostr-session-jwt.guard'; import { UsersModule } from 'src/users/users.module'; @@ -30,6 +31,7 @@ import { UsersModule } from 'src/users/users.module'; JwtAuthGuard, TokenAuthGuard, HybridAuthGuard, + OptionalHybridAuthGuard, NostrSessionService, NostrSessionJwtGuard, ], @@ -39,6 +41,7 @@ import { UsersModule } from 'src/users/users.module'; JwtAuthGuard, TokenAuthGuard, HybridAuthGuard, + OptionalHybridAuthGuard, NostrSessionService, NostrSessionJwtGuard, ], diff --git a/backend/src/auth/guards/optional-hybrid-auth.guard.ts b/backend/src/auth/guards/optional-hybrid-auth.guard.ts new file mode 100644 index 0000000..59788ca --- /dev/null +++ b/backend/src/auth/guards/optional-hybrid-auth.guard.ts @@ -0,0 +1,30 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { HybridAuthGuard } from './hybrid-auth.guard'; + +/** + * Optional version of HybridAuthGuard. + * + * Tries all authentication strategies (Nostr, NostrSessionJwt, Jwt). + * If any succeeds, `request.user` is populated as normal. + * If all fail, the request proceeds anyway with `request.user = undefined`. + * + * Use this for endpoints that should work for both authenticated and + * anonymous users (e.g. streaming free content without login). + */ +@Injectable() +export class OptionalHybridAuthGuard implements CanActivate { + constructor(private readonly hybridAuthGuard: HybridAuthGuard) {} + + async canActivate(context: ExecutionContext): Promise { + try { + await this.hybridAuthGuard.canActivate(context); + } catch { + // Auth failed — that's OK for optional auth. + // Ensure request.user is explicitly undefined so downstream + // code can check whether the user is authenticated. + const request = context.switchToHttp().getRequest(); + request.user = undefined; + } + return true; + } +} diff --git a/backend/src/common/validate-env.ts b/backend/src/common/validate-env.ts new file mode 100644 index 0000000..60d05c0 --- /dev/null +++ b/backend/src/common/validate-env.ts @@ -0,0 +1,73 @@ +import { Logger } from '@nestjs/common'; + +/** + * Validates that all required environment variables are present at startup. + * Fails fast with a clear error message listing every missing variable, + * so misconfigured Portainer deployments are caught immediately. + */ +export function validateEnvironment(): void { + const logger = new Logger('EnvironmentValidation'); + + const required: string[] = [ + 'ENVIRONMENT', + 'DATABASE_HOST', + 'DATABASE_PORT', + 'DATABASE_USER', + 'DATABASE_PASSWORD', + 'DATABASE_NAME', + 'QUEUE_HOST', + 'QUEUE_PORT', + 'AWS_ACCESS_KEY', + 'AWS_SECRET_KEY', + 'S3_PRIVATE_BUCKET_NAME', + 'S3_PUBLIC_BUCKET_NAME', + 'NOSTR_JWT_SECRET', + 'AES_MASTER_SECRET', + ]; + + // BTCPay is required for payment processing + const btcpayVars = [ + 'BTCPAY_URL', + 'BTCPAY_API_KEY', + 'BTCPAY_STORE_ID', + 'BTCPAY_WEBHOOK_SECRET', + ]; + + const missing = required.filter((key) => !process.env[key]); + const missingBtcpay = btcpayVars.filter((key) => !process.env[key]); + + if (missingBtcpay.length > 0 && missingBtcpay.length < btcpayVars.length) { + // Some BTCPay vars set but not all — likely a partial config error + logger.error( + `Partial BTCPay configuration detected. Missing: ${missingBtcpay.join(', ')}. ` + + 'Set all BTCPay variables or leave all empty to disable payments.', + ); + missing.push(...missingBtcpay); + } + + if (missing.length > 0) { + const message = + `Missing required environment variables:\n` + + missing.map((key) => ` - ${key}`).join('\n') + + `\n\nSet these in Portainer Stack environment variables before deploying.`; + + logger.error(message); + throw new Error(message); + } + + // Warn about insecure defaults + const insecureDefaults: Record = { + NOSTR_JWT_SECRET: 'change-this-to-a-long-random-secret-in-production', + AES_MASTER_SECRET: 'change-this-32-byte-hex-secret-00', + }; + + for (const [key, insecureValue] of Object.entries(insecureDefaults)) { + if (process.env[key] === insecureValue) { + logger.warn( + `${key} is using an insecure default value. Generate a secure secret with: openssl rand -hex 32`, + ); + } + } + + logger.log('Environment validation passed'); +} diff --git a/backend/src/contents/contents.controller.ts b/backend/src/contents/contents.controller.ts index 47ca504..ea0f08a 100644 --- a/backend/src/contents/contents.controller.ts +++ b/backend/src/contents/contents.controller.ts @@ -9,8 +9,11 @@ import { UseInterceptors, Post, Logger, + Req, + UnauthorizedException, } from '@nestjs/common'; import { HybridAuthGuard } from 'src/auth/guards/hybrid-auth.guard'; +import { OptionalHybridAuthGuard } from 'src/auth/guards/optional-hybrid-auth.guard'; import { Subscriptions } from 'src/subscriptions/decorators/subscriptions.decorator'; import { SubscriptionsGuard } from 'src/subscriptions/guards/subscription.guard'; import { ContentsService } from './contents.service'; @@ -69,10 +72,25 @@ export class ContentsController { } @Get(':id/stream') - @UseGuards(HybridAuthGuard, SubscriptionsGuard) - @Subscriptions(['enthusiast', 'film-buff', 'cinephile']) - async stream(@Param('id') id: string) { + @UseGuards(OptionalHybridAuthGuard) + async stream( + @Param('id') id: string, + @Req() req: any, + ) { const content = await this.contentsService.stream(id); + + // Determine if the content is free (no payment required) + const projectPrice = Number(content.project?.rentalPrice ?? 0); + const contentPrice = Number(content.rentalPrice ?? 0); + const isFreeContent = projectPrice <= 0 && contentPrice <= 0; + + // Paid content requires a valid authenticated user + if (!isFreeContent && !req.user) { + throw new UnauthorizedException( + 'Authentication required for paid content', + ); + } + const dto = new StreamContentDTO(content); // Check if the HLS manifest actually exists in the public bucket. @@ -90,7 +108,11 @@ export class ContentsController { ); if (hlsExists) { - return dto; + // Return the public S3 URL for the HLS manifest so the player + // can fetch it directly from MinIO/S3 (avoids proxying through + // the API and prevents CORS issues with relative segment paths). + const publicUrl = getPublicS3Url(outputKey); + return { ...dto, file: publicUrl }; } // HLS not available — serve a presigned URL for the original file diff --git a/backend/src/main.ts b/backend/src/main.ts index 7b171bf..62d28e3 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -3,15 +3,18 @@ import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe } from '@nestjs/common'; import { useContainer } from 'class-validator'; -// Sentry instrumentation removed (see instrument.ts) import * as express from 'express'; import { ExpressAdapter, NestExpressApplication, } from '@nestjs/platform-express'; import { RawBodyRequest } from './types/raw-body-request'; +import { validateEnvironment } from './common/validate-env'; async function bootstrap() { + // Fail fast if critical env vars are missing + validateEnvironment(); + const server = express(); const captureRawBody = ( request: RawBodyRequest, @@ -46,7 +49,10 @@ async function bootstrap() { useContainer(app.select(AppModule), { fallbackOnErrors: true }); - if (process.env.ENVIRONMENT === 'development') { + if ( + process.env.ENVIRONMENT === 'development' || + process.env.ENVIRONMENT === 'local' + ) { const swagConfig = new DocumentBuilder() .setTitle('IndeeHub API') .setDescription('This is the API for the IndeeHub application') @@ -61,19 +67,29 @@ async function bootstrap() { app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); if (process.env.ENVIRONMENT === 'production') { + // Build CORS origin list from FRONTEND_URL + known domains + const origins: string[] = [ + 'https://indeehub.studio', + 'https://www.indeehub.studio', + 'https://app.indeehub.studio', + 'https://bff.indeehub.studio', + ]; + + if (process.env.FRONTEND_URL) { + origins.push(process.env.FRONTEND_URL); + } + if (process.env.DOMAIN) { + origins.push(`https://${process.env.DOMAIN}`); + } + app.enableCors({ - origin: [ - 'https://indeehub.studio', - 'https://www.indeehub.studio', - 'https://app.indeehub.studio', - 'https://bff.indeehub.studio', - 'https://indeehub.retool.com', - 'https://www.indeehub.retool.com', - ], + origin: [...new Set(origins)], methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], credentials: true, }); - } else app.enableCors(); + } else { + app.enableCors(); + } await app.listen(process.env.PORT || 4000); } diff --git a/backend/src/projects/dto/response/base-project.dto.ts b/backend/src/projects/dto/response/base-project.dto.ts index 2d2e8aa..bed2720 100644 --- a/backend/src/projects/dto/response/base-project.dto.ts +++ b/backend/src/projects/dto/response/base-project.dto.ts @@ -22,6 +22,7 @@ export class BaseProjectDTO { synopsis?: string; trailer?: string; poster?: string; + streamingUrl?: string; status: Status; type: Type; format?: Format; @@ -58,11 +59,14 @@ export class BaseProjectDTO { } else { // Pick the best content for the film slot. When a project has // multiple content rows (e.g. an auto-created placeholder plus - // the real upload), prefer the one with a rental price set or - // a file uploaded rather than blindly taking contents[0]. + // the real upload), prefer completed content with a file. const best = project.contents?.length ? [...project.contents].sort((a, b) => { - // Prefer content with a file + // Prefer completed content first + const aCompleted = a.status === 'completed' ? 1 : 0; + const bCompleted = b.status === 'completed' ? 1 : 0; + if (bCompleted !== aCompleted) return bCompleted - aCompleted; + // Then prefer content with a file const aFile = a.file ? 1 : 0; const bFile = b.file ? 1 : 0; if (bFile !== aFile) return bFile - aFile; @@ -88,7 +92,20 @@ export class BaseProjectDTO { } if (project.poster) { - this.poster = getPublicS3Url(project.poster); + // External URLs and local static paths (starting with / or http) + // should not be transformed through the S3 bucket URL + if ( + project.poster.startsWith('http') || + project.poster.startsWith('/') + ) { + this.poster = project.poster; + } else { + this.poster = getPublicS3Url(project.poster); + } + } + + if (project.streamingUrl) { + this.streamingUrl = project.streamingUrl; } this.screenings = project.screenings diff --git a/backend/src/projects/projects.service.ts b/backend/src/projects/projects.service.ts index 420f9ee..22f9f01 100644 --- a/backend/src/projects/projects.service.ts +++ b/backend/src/projects/projects.service.ts @@ -171,6 +171,9 @@ export class ProjectsService { 'contents.poster', ]); + // Always load the genre so the DTO can include it in the response + projectsQuery.leftJoinAndSelect('project.genre', 'genre'); + if (query.status) { if (query.status === 'published') { const completed = this.contentsService.getCompletedProjectsSubquery(); @@ -185,7 +188,7 @@ export class ProjectsService { if (query.search) { projectsQuery.leftJoin('project.projectSubgenres', 'projectSubgenres'); projectsQuery.leftJoin('projectSubgenres.subgenre', 'subgenre'); - projectsQuery.leftJoin('project.genre', 'genre'); + // genre already joined above via leftJoinAndSelect projectsQuery.leftJoin('contents.cast', 'castMembers'); projectsQuery.leftJoin('castMembers.filmmaker', 'castFilmmaker'); diff --git a/backend/src/scripts/seed-content.ts b/backend/src/scripts/seed-content.ts index 56f6477..5e88ec7 100644 --- a/backend/src/scripts/seed-content.ts +++ b/backend/src/scripts/seed-content.ts @@ -4,9 +4,11 @@ * Populates the PostgreSQL database with: * 1. Genres (Documentary, Drama, etc.) * 2. Test users with Nostr pubkeys and active subscriptions - * 3. IndeeHub films (native delivery mode) - * 4. TopDoc films (native delivery mode, YouTube streaming URLs) - * 5. Projects and contents for both film sets + * 3. IndeeHub films (native delivery mode, free) + * + * TopDoc documentary films are NOT seeded — they live in the frontend + * mock data (src/data/topDocFilms.ts) and appear only when the user + * switches to the "TopDoc Films" content source. * * Run: node dist/scripts/seed-content.js * Requires: DATABASE_HOST, DATABASE_PORT, DATABASE_USER, etc. in env @@ -24,7 +26,6 @@ const client = new Client({ }); // ── Test Users ──────────────────────────────────────────────── -// Using the same dev personas from the frontend seed const testUsers = [ { id: randomUUID(), @@ -70,7 +71,6 @@ const indeeHubFilms = [ 'A groundbreaking documentary exploring the intersection of faith, finance, and the future of money through the lens of Bitcoin.', poster: '/images/films/posters/god-bless-bitcoin.webp', genre: 'Documentary', - categories: ['Documentary', 'Bitcoin', 'Religion'], deliveryMode: 'native', }, { @@ -80,7 +80,6 @@ const indeeHubFilms = [ 'A compelling narrative exploring the emotional weight of our past.', poster: '/images/films/posters/thethingswecarry.webp', genre: 'Drama', - categories: ['Drama'], deliveryMode: 'native', }, { @@ -89,7 +88,6 @@ const indeeHubFilms = [ synopsis: 'An intense confrontation that tests the limits of human resolve.', poster: '/images/films/posters/duel.png', genre: 'Action', - categories: ['Drama', 'Action'], deliveryMode: 'native', }, { @@ -100,66 +98,6 @@ const indeeHubFilms = [ poster: '/images/films/posters/2b0d7349-c010-47a0-b584-49e1bf86ab2f.png', genre: 'Documentary', - categories: ['Documentary', 'Finance', 'Bitcoin'], - deliveryMode: 'native', - }, -]; - -// ── TopDoc Films ────────────────────────────────────────────── -const topDocFilms = [ - { - id: 'tdf-god-bless-bitcoin', - title: 'God Bless Bitcoin', - synopsis: - 'Exploring the intersection of faith and Bitcoin.', - poster: '/images/films/posters/topdoc/god-bless-bitcoin.jpg', - streamingUrl: 'https://www.youtube.com/embed/3XEuqixD2Zg', - genre: 'Documentary', - categories: ['Documentary', 'Bitcoin'], - deliveryMode: 'native', - }, - { - id: 'tdf-bitcoin-end-of-money', - title: 'Bitcoin: The End of Money as We Know It', - synopsis: - 'Tracing the history of money from barter to Bitcoin.', - poster: '/images/films/posters/topdoc/bitcoin-end-of-money.jpg', - streamingUrl: 'https://www.youtube.com/embed/zpNlG3VtcBM', - genre: 'Documentary', - categories: ['Documentary', 'Bitcoin', 'Economics'], - deliveryMode: 'native', - }, - { - id: 'tdf-bitcoin-beyond-bubble', - title: 'Bitcoin: Beyond the Bubble', - synopsis: - 'An accessible explainer tracing currency evolution.', - poster: '/images/films/posters/topdoc/bitcoin-beyond-bubble.jpg', - streamingUrl: 'https://www.youtube.com/embed/URrmfEu0cZ8', - genre: 'Documentary', - categories: ['Documentary', 'Bitcoin', 'Economics'], - deliveryMode: 'native', - }, - { - id: 'tdf-bitcoin-gospel', - title: 'The Bitcoin Gospel', - synopsis: - 'The true believers argue Bitcoin is a gamechanger for the global economy.', - poster: '/images/films/posters/topdoc/bitcoin-gospel.jpg', - streamingUrl: 'https://www.youtube.com/embed/2I6dXRK9oJo', - genre: 'Documentary', - categories: ['Documentary', 'Bitcoin'], - deliveryMode: 'native', - }, - { - id: 'tdf-banking-on-bitcoin', - title: 'Banking on Bitcoin', - synopsis: - 'Chronicles idealists and entrepreneurs as they redefine money.', - poster: '/images/films/posters/topdoc/banking-on-bitcoin.jpg', - streamingUrl: 'https://www.youtube.com/embed/BbMT1Mhv7OQ', - genre: 'Documentary', - categories: ['Documentary', 'Bitcoin', 'Finance'], deliveryMode: 'native', }, ]; @@ -169,7 +107,6 @@ async function seed() { await client.connect(); try { - // Run inside a transaction await client.query('BEGIN'); // 1. Seed genres @@ -242,47 +179,15 @@ async function seed() { ], ); - // Create a content record for the film + // Content with status 'completed' so it appears in public API listings const contentId = `content-${film.id}`; await client.query( - `INSERT INTO contents (id, project_id, title, synopsis, status, "order", release_date, created_at, updated_at) - VALUES ($1, $2, $3, $4, 'ready', 1, NOW(), NOW(), NOW()) - ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title`, - [contentId, film.id, film.title, film.synopsis], - ); - } - - // 5. Seed TopDoc films - console.log('[seed] Seeding TopDoc films...'); - for (const film of topDocFilms) { - const genreId = genreLookup[film.genre] || null; - await client.query( - `INSERT INTO projects (id, name, title, slug, synopsis, poster, status, type, genre_id, delivery_mode, streaming_url, created_at, updated_at) - VALUES ($1, $2, $3, $4, $5, $6, 'published', 'film', $7, $8, $9, NOW(), NOW()) + `INSERT INTO contents (id, project_id, title, synopsis, status, "order", rental_price, release_date, created_at, updated_at) + VALUES ($1, $2, $3, $4, 'completed', 1, 0, NOW(), NOW(), NOW()) ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title, - synopsis = EXCLUDED.synopsis, - poster = EXCLUDED.poster, - delivery_mode = EXCLUDED.delivery_mode, - streaming_url = EXCLUDED.streaming_url`, - [ - film.id, - film.title, - film.title, - film.id, - film.synopsis, - film.poster, - genreId, - film.deliveryMode, - film.streamingUrl, - ], - ); - - const contentId = `content-${film.id}`; - await client.query( - `INSERT INTO contents (id, project_id, title, synopsis, status, "order", release_date, created_at, updated_at) - VALUES ($1, $2, $3, $4, 'ready', 1, NOW(), NOW(), NOW()) - ON CONFLICT (id) DO UPDATE SET title = EXCLUDED.title`, + status = 'completed', + rental_price = 0`, [contentId, film.id, film.title, film.synopsis], ); } @@ -292,7 +197,6 @@ async function seed() { console.log(` - ${genres.length} genres`); console.log(` - ${testUsers.length} test users with subscriptions`); console.log(` - ${indeeHubFilms.length} IndeeHub films`); - console.log(` - ${topDocFilms.length} TopDoc films`); } catch (error) { await client.query('ROLLBACK'); console.error('[seed] Error seeding database:', error); diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..cecd48b --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,186 @@ +version: '3.8' + +services: + # ── Frontend (nginx serving built Vue app) ─────────────────── + app: + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "7777:7777" + depends_on: + - relay + - api + networks: + - indeedhub-network + labels: + - "com.centurylinklabs.watchtower.enable=true" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:7777/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # ── Backend API (NestJS) ───────────────────────────────────── + api: + build: + context: ./backend + dockerfile: Dockerfile + restart: unless-stopped + env_file: + - ./backend/.env + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + minio: + condition: service_started + networks: + - indeedhub-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:4000/nostr-auth/health"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + + # ── PostgreSQL Database ────────────────────────────────────── + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: indeedhub + POSTGRES_PASSWORD: indeedhub_dev_2026 + POSTGRES_DB: indeedhub + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - indeedhub-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U indeedhub"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + + # ── Redis (BullMQ job queue) ───────────────────────────────── + redis: + image: redis:7-alpine + restart: unless-stopped + volumes: + - redis-data:/data + networks: + - indeedhub-network + + # ── MinIO (S3-compatible object storage) ───────────────────── + minio: + image: minio/minio:latest + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123 + volumes: + - minio-data:/data + ports: + - "9001:9001" + networks: + - indeedhub-network + + # ── MinIO bucket init (one-shot: creates required buckets) ─── + minio-init: + image: minio/mc:latest + depends_on: + - minio + entrypoint: > + /bin/sh -c " + sleep 5; + mc alias set local http://minio:9000 minioadmin minioadmin123; + mc mb local/indeedhub-private --ignore-existing; + mc mb local/indeedhub-public --ignore-existing; + mc anonymous set download local/indeedhub-public; + echo 'MinIO buckets initialized'; + " + networks: + - indeedhub-network + restart: "no" + + # ── FFmpeg Transcoding Worker ──────────────────────────────── + ffmpeg-worker: + build: + context: ./backend + dockerfile: Dockerfile.ffmpeg + restart: unless-stopped + env_file: + - ./backend/.env + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + minio: + condition: service_started + networks: + - indeedhub-network + + # ── Mailpit (development email testing) ────────────────────── + mailpit: + image: axllent/mailpit:latest + restart: unless-stopped + ports: + - "8025:8025" + networks: + - indeedhub-network + + # ── Nostr Relay (stores comments, reactions, profiles) ─────── + relay: + image: scsibug/nostr-rs-relay:latest + restart: unless-stopped + volumes: + - relay-data:/usr/src/app/db + networks: + - indeedhub-network + + # ── Seeder (one-shot: seeds test data into relay, then exits) + seeder: + build: + context: . + dockerfile: Dockerfile.seed + depends_on: + - relay + environment: + - RELAY_URL=ws://relay:8080 + - ORIGIN=http://localhost:7777 + networks: + - indeedhub-network + restart: "no" + + # ── DB Seeder (one-shot: seeds content into PostgreSQL) ────── + db-seeder: + build: + context: ./backend + dockerfile: Dockerfile + depends_on: + postgres: + condition: service_healthy + minio: + condition: service_started + env_file: + - ./backend/.env + command: ["node", "dist/scripts/seed-content.js"] + networks: + - indeedhub-network + restart: "no" + +networks: + indeedhub-network: + driver: bridge + +volumes: + postgres-data: + redis-data: + minio-data: + relay-data: diff --git a/docker-compose.yml b/docker-compose.yml index cecd48b..d8cb5eb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,38 @@ +# ═══════════════════════════════════════════════════════════════ +# IndeeHub — Production Stack for Portainer +# ═══════════════════════════════════════════════════════════════ +# +# All ${VARIABLES} are resolved by Portainer at deploy time. +# Configure them in Portainer → Stacks → Environment variables +# before deploying. +# +# See .env.portainer for the full list of required variables. +# +# For local development, use: docker compose -f docker-compose.dev.yml up +# ═══════════════════════════════════════════════════════════════ + version: '3.8' services: - # ── Frontend (nginx serving built Vue app) ─────────────────── + # ── Frontend (nginx serving built Vue app) ────────────────── app: build: context: . dockerfile: Dockerfile + args: + VITE_USE_MOCK_DATA: "false" + VITE_CONTENT_ORIGIN: ${FRONTEND_URL} + VITE_INDEEHUB_API_URL: /api + VITE_INDEEHUB_CDN_URL: /storage + VITE_NOSTR_RELAYS: "" restart: unless-stopped ports: - - "7777:7777" + - "${APP_PORT:-7777}:7777" depends_on: - relay - api networks: - indeedhub-network - labels: - - "com.centurylinklabs.watchtower.enable=true" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:7777/health"] interval: 30s @@ -23,14 +40,106 @@ services: retries: 3 start_period: 40s - # ── Backend API (NestJS) ───────────────────────────────────── + # ── Backend API (NestJS) ──────────────────────────────────── api: build: context: ./backend dockerfile: Dockerfile restart: unless-stopped - env_file: - - ./backend/.env + environment: + # ── Core ───────────────────────────────────────────── + ENVIRONMENT: production + PORT: 4000 + DOMAIN: ${DOMAIN} + FRONTEND_URL: ${FRONTEND_URL} + + # ── Database ───────────────────────────────────────── + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_USER: ${POSTGRES_USER} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_NAME: ${POSTGRES_DB} + + # ── Redis / BullMQ ─────────────────────────────────── + QUEUE_HOST: redis + QUEUE_PORT: 6379 + QUEUE_PASSWORD: ${REDIS_PASSWORD:-} + + # ── S3 / MinIO ────────────────────────────────────── + S3_ENDPOINT: ${S3_ENDPOINT:-http://minio:9000} + AWS_REGION: ${AWS_REGION:-us-east-1} + AWS_ACCESS_KEY: ${S3_ACCESS_KEY} + AWS_SECRET_KEY: ${S3_SECRET_KEY} + S3_PRIVATE_BUCKET_NAME: ${S3_PRIVATE_BUCKET:-indeedhub-private} + S3_PUBLIC_BUCKET_NAME: ${S3_PUBLIC_BUCKET:-indeedhub-public} + S3_PUBLIC_BUCKET_URL: ${S3_PUBLIC_BUCKET_URL} + + # ── CloudFront (leave empty for MinIO/self-hosted) ── + CLOUDFRONT_PRIVATE_KEY: ${CLOUDFRONT_PRIVATE_KEY:-} + CLOUDFRONT_KEY_PAIR_ID: ${CLOUDFRONT_KEY_PAIR_ID:-} + CLOUDFRONT_DISTRIBUTION_URL: ${CLOUDFRONT_DISTRIBUTION_URL:-} + + # ── BTCPay Server ─────────────────────────────────── + BTCPAY_URL: ${BTCPAY_URL} + BTCPAY_API_KEY: ${BTCPAY_API_KEY} + BTCPAY_STORE_ID: ${BTCPAY_STORE_ID} + BTCPAY_WEBHOOK_SECRET: ${BTCPAY_WEBHOOK_SECRET} + BTCPAY_ROUTE_HINTS: ${BTCPAY_ROUTE_HINTS:-false} + + # ── Nostr Auth / JWT ───────────────────────────────── + NOSTR_JWT_SECRET: ${NOSTR_JWT_SECRET} + NOSTR_JWT_EXPIRES_IN: ${NOSTR_JWT_EXPIRES_IN:-7d} + + # ── AES-128 Content Encryption ────────────────────── + AES_MASTER_SECRET: ${AES_MASTER_SECRET} + + # ── SMTP / Email ───────────────────────────────────── + SMTP_HOST: ${SMTP_HOST:-} + SMTP_PORT: ${SMTP_PORT:-587} + SMTP_USER: ${SMTP_USER:-} + SMTP_PASS: ${SMTP_PASS:-} + MAIL_FROM: ${MAIL_FROM:-noreply@indeedhub.local} + + # ── SendGrid (optional -- alternative to SMTP) ────── + SENDGRID_API_KEY: ${SENDGRID_API_KEY:-} + SENDGRID_SENDER: ${SENDGRID_SENDER:-} + + # ── Cognito (optional -- disabled with Nostr auth) ── + COGNITO_USER_POOL_ID: ${COGNITO_USER_POOL_ID:-} + COGNITO_CLIENT_ID: ${COGNITO_CLIENT_ID:-} + + # ── Flash Subscription Secrets (optional) ─────────── + FLASH_JWT_SECRET_ENTHUSIAST: ${FLASH_JWT_SECRET_ENTHUSIAST:-} + FLASH_JWT_SECRET_FILM_BUFF: ${FLASH_JWT_SECRET_FILM_BUFF:-} + FLASH_JWT_SECRET_CINEPHILE: ${FLASH_JWT_SECRET_CINEPHILE:-} + FLASH_JWT_SECRET_RSS_ADDON: ${FLASH_JWT_SECRET_RSS_ADDON:-} + FLASH_JWT_SECRET_VERIFICATION_ADDON: ${FLASH_JWT_SECRET_VERIFICATION_ADDON:-} + + # ── Transcoding API (optional) ────────────────────── + TRANSCODING_API_KEY: ${TRANSCODING_API_KEY:-} + TRANSCODING_API_URL: ${TRANSCODING_API_URL:-} + + # ── PostHog Analytics (optional) ──────────────────── + POSTHOG_API_KEY: ${POSTHOG_API_KEY:-} + + # ── Sentry Error Tracking (optional) ──────────────── + SENTRY_ENVIRONMENT: ${SENTRY_ENVIRONMENT:-production} + + # ── DRM (optional) ────────────────────────────────── + DRM_SECRET_NAME: ${DRM_SECRET_NAME:-} + PRIVATE_AUTH_CERTIFICATE_KEY_ID: ${PRIVATE_AUTH_CERTIFICATE_KEY_ID:-} + + # ── Podping (optional) ────────────────────────────── + PODPING_URL: ${PODPING_URL:-} + PODPING_KEY: ${PODPING_KEY:-} + PODPING_USER_AGENT: ${PODPING_USER_AGENT:-} + + # ── Admin API (optional) ──────────────────────────── + ADMIN_API_KEY: ${ADMIN_API_KEY:-} + + # ── Partner Content (optional) ────────────────────── + PARTNER_API_BASE_URL: ${PARTNER_API_BASE_URL:-} + PARTNER_API_KEY: ${PARTNER_API_KEY:-} depends_on: postgres: condition: service_healthy @@ -47,50 +156,56 @@ services: retries: 5 start_period: 60s - # ── PostgreSQL Database ────────────────────────────────────── + # ── PostgreSQL Database ───────────────────────────────────── postgres: image: postgres:16-alpine restart: unless-stopped environment: - POSTGRES_USER: indeedhub - POSTGRES_PASSWORD: indeedhub_dev_2026 - POSTGRES_DB: indeedhub + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} volumes: - postgres-data:/var/lib/postgresql/data networks: - indeedhub-network healthcheck: - test: ["CMD-SHELL", "pg_isready -U indeedhub"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 10s timeout: 5s retries: 5 start_period: 30s - # ── Redis (BullMQ job queue) ───────────────────────────────── + # ── Redis (BullMQ job queue) ──────────────────────────────── redis: image: redis:7-alpine restart: unless-stopped + command: > + sh -c "if [ -n '${REDIS_PASSWORD:-}' ]; then + redis-server --requirepass '${REDIS_PASSWORD}' --appendonly yes; + else + redis-server --appendonly yes; + fi" volumes: - redis-data:/data networks: - indeedhub-network - # ── MinIO (S3-compatible object storage) ───────────────────── + # ── MinIO (S3-compatible object storage) ──────────────────── minio: image: minio/minio:latest restart: unless-stopped command: server /data --console-address ":9001" environment: - MINIO_ROOT_USER: minioadmin - MINIO_ROOT_PASSWORD: minioadmin123 + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} volumes: - minio-data:/data ports: - - "9001:9001" + - "${MINIO_CONSOLE_PORT:-9001}:9001" networks: - indeedhub-network - # ── MinIO bucket init (one-shot: creates required buckets) ─── + # ── MinIO bucket init (one-shot: creates required buckets) ── minio-init: image: minio/mc:latest depends_on: @@ -98,24 +213,41 @@ services: entrypoint: > /bin/sh -c " sleep 5; - mc alias set local http://minio:9000 minioadmin minioadmin123; - mc mb local/indeedhub-private --ignore-existing; - mc mb local/indeedhub-public --ignore-existing; - mc anonymous set download local/indeedhub-public; + mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD}; + mc mb local/${S3_PRIVATE_BUCKET:-indeedhub-private} --ignore-existing; + mc mb local/${S3_PUBLIC_BUCKET:-indeedhub-public} --ignore-existing; + mc anonymous set download local/${S3_PUBLIC_BUCKET:-indeedhub-public}; echo 'MinIO buckets initialized'; " networks: - indeedhub-network restart: "no" - # ── FFmpeg Transcoding Worker ──────────────────────────────── + # ── FFmpeg Transcoding Worker ─────────────────────────────── ffmpeg-worker: build: context: ./backend dockerfile: Dockerfile.ffmpeg restart: unless-stopped - env_file: - - ./backend/.env + environment: + # Worker shares database + S3 + Redis config with the API + ENVIRONMENT: production + DATABASE_HOST: postgres + DATABASE_PORT: 5432 + DATABASE_USER: ${POSTGRES_USER} + DATABASE_PASSWORD: ${POSTGRES_PASSWORD} + DATABASE_NAME: ${POSTGRES_DB} + QUEUE_HOST: redis + QUEUE_PORT: 6379 + QUEUE_PASSWORD: ${REDIS_PASSWORD:-} + S3_ENDPOINT: ${S3_ENDPOINT:-http://minio:9000} + AWS_REGION: ${AWS_REGION:-us-east-1} + AWS_ACCESS_KEY: ${S3_ACCESS_KEY} + AWS_SECRET_KEY: ${S3_SECRET_KEY} + S3_PRIVATE_BUCKET_NAME: ${S3_PRIVATE_BUCKET:-indeedhub-private} + S3_PUBLIC_BUCKET_NAME: ${S3_PUBLIC_BUCKET:-indeedhub-public} + S3_PUBLIC_BUCKET_URL: ${S3_PUBLIC_BUCKET_URL} + AES_MASTER_SECRET: ${AES_MASTER_SECRET} depends_on: postgres: condition: service_healthy @@ -126,16 +258,7 @@ services: networks: - indeedhub-network - # ── Mailpit (development email testing) ────────────────────── - mailpit: - image: axllent/mailpit:latest - restart: unless-stopped - ports: - - "8025:8025" - networks: - - indeedhub-network - - # ── Nostr Relay (stores comments, reactions, profiles) ─────── + # ── Nostr Relay ───────────────────────────────────────────── relay: image: scsibug/nostr-rs-relay:latest restart: unless-stopped @@ -144,37 +267,6 @@ services: networks: - indeedhub-network - # ── Seeder (one-shot: seeds test data into relay, then exits) - seeder: - build: - context: . - dockerfile: Dockerfile.seed - depends_on: - - relay - environment: - - RELAY_URL=ws://relay:8080 - - ORIGIN=http://localhost:7777 - networks: - - indeedhub-network - restart: "no" - - # ── DB Seeder (one-shot: seeds content into PostgreSQL) ────── - db-seeder: - build: - context: ./backend - dockerfile: Dockerfile - depends_on: - postgres: - condition: service_healthy - minio: - condition: service_started - env_file: - - ./backend/.env - command: ["node", "dist/scripts/seed-content.js"] - networks: - - indeedhub-network - restart: "no" - networks: indeedhub-network: driver: bridge diff --git a/nginx.conf b/nginx.conf index 8bc1a01..1ff883d 100644 --- a/nginx.conf +++ b/nginx.conf @@ -42,8 +42,8 @@ server { proxy_read_timeout 300s; proxy_send_timeout 300s; - # Handle large uploads - client_max_body_size 100m; + # Handle large video uploads (up to 5GB) + client_max_body_size 5g; } # ── MinIO storage proxy (public bucket) ──────────────────── diff --git a/scripts/dev.sh b/scripts/dev.sh index d68f4b4..809fb44 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -65,7 +65,7 @@ cleanup() { # Stop Docker infrastructure (if we used Docker) if [ "$SKIP_DOCKER" = false ] && command -v docker &>/dev/null; then echo -e "${CYAN}Stopping Docker services...${NC}" - docker compose -f "$ROOT_DIR/docker-compose.yml" stop postgres redis minio minio-init mailpit 2>/dev/null || true + docker compose -f "$ROOT_DIR/docker-compose.dev.yml" stop postgres redis minio minio-init mailpit 2>/dev/null || true fi # Kill anything still on relay port @@ -198,12 +198,12 @@ if [ "$SKIP_DOCKER" = true ] || [ "$HAS_DOCKER" = false ]; then else echo -e "${CYAN}[1/4] Starting Docker infrastructure...${NC}" - docker compose -f "$ROOT_DIR/docker-compose.yml" up -d \ + docker compose -f "$ROOT_DIR/docker-compose.dev.yml" up -d \ postgres redis minio minio-init mailpit 2>&1 | prefix_output "docker" "$CYAN" echo -e "${CYAN} Waiting for Postgres...${NC}" for i in $(seq 1 30); do - if docker compose -f "$ROOT_DIR/docker-compose.yml" exec -T postgres pg_isready -U indeedhub -q 2>/dev/null; then + if docker compose -f "$ROOT_DIR/docker-compose.dev.yml" exec -T postgres pg_isready -U indeedhub -q 2>/dev/null; then echo -e "${GREEN} Postgres is ready.${NC}" break fi diff --git a/src/components/ContentDetailModal.vue b/src/components/ContentDetailModal.vue index 6040d33..bf87d9e 100644 --- a/src/components/ContentDetailModal.vue +++ b/src/components/ContentDetailModal.vue @@ -302,8 +302,11 @@ async function checkRentalAccess() { const result = await libraryService.checkRentExists(contentId) hasActiveRental.value = result.exists rentalExpiresAt.value = result.expiresAt ? new Date(result.expiresAt) : null - } catch { - hasActiveRental.value = false + } catch (err) { + console.warn('Rental check failed:', err) + // If the rental check fails (e.g. auth issue) but the user owns the + // content, treat it as "can play" so the owner isn't blocked. + hasActiveRental.value = !!props.content.isOwnProject rentalExpiresAt.value = null } } @@ -356,8 +359,16 @@ function getProfile(pubkey: string) { } function handlePlay() { - // Free content with a streaming URL can play without auth - if (props.content?.streamingUrl) { + // Free content (YouTube embeds or rentalPrice = 0) plays without auth + const isFree = props.content?.streamingUrl || + !props.content?.rentalPrice || props.content.rentalPrice <= 0 + if (isFree) { + showVideoPlayer.value = true + return + } + + // Content creators can always preview/play their own content + if (props.content?.isOwnProject) { showVideoPlayer.value = true return } diff --git a/src/components/VideoPlayer.vue b/src/components/VideoPlayer.vue index bae15a4..01af378 100644 --- a/src/components/VideoPlayer.vue +++ b/src/components/VideoPlayer.vue @@ -215,7 +215,7 @@