refactor: update environment configuration and documentation

- Modified `.env.example` to reflect new API URL structure and added CDN configuration for external storage.
- Updated `.gitignore` to include deployment secrets and certificate files, ensuring sensitive information is not committed.
- Revised `BACKEND_INTEGRATION.md` to clarify authentication methods, replacing Cognito references with Nostr NIP-98.
- Deleted outdated documentation files (`CONTENT-INTEGRATION-COMPLETE.md`, `CURSOR-MCP-SETUP.md`, `FINAL-STATUS.md`, `FIXES-APPLIED.md`, `INDEEHHUB-INTEGRATION.md`, `PROJECT-COMPLETE.md`, `PROJECT-SUMMARY.md`) to streamline project documentation.

These changes enhance the clarity of the environment setup and improve the overall documentation structure for better developer onboarding.
This commit is contained in:
Dorian
2026-02-17 05:12:59 +00:00
parent a88022f81d
commit 8d56fe392d
29 changed files with 782 additions and 1713 deletions

View File

@@ -1,101 +1,44 @@
ENVIRONMENT=local # local | development | production
ENVIRONMENT=local
# App - Local
# App
PORT=4000
DOMAIN=localhost:4000
FRONTEND_URL=http://localhost:3000
FRONTEND_URL=http://localhost:5174
# DB - API
# PostgreSQL
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_USER=postgres
DATABASE_PASSWORD=local
DATABASE_NAME=indeehub
DATABASE_USER=indeedhub
DATABASE_PASSWORD=your_password
DATABASE_NAME=indeedhub
# DB - EVENTS/ANALYTICS (POSTHOG)
DATABASE_POSTHOG_HOST=localhost
DATABASE_POSTHOG_PORT=5434
DATABASE_POSTHOG_USER=postgres
DATABASE_POSTHOG_PASSWORD=local
DATABASE_POSTHOG_NAME=staging
# Trascoding Queue - Local
# Redis (BullMQ)
QUEUE_HOST=localhost
QUEUE_PORT=6379
QUEUE_PASSWORD=
# BTCPay Server - Bitcoin/Lightning Payments
BTCPAY_URL=https://btcpay.yourdomain.com
# MinIO (S3-compatible storage)
S3_ENDPOINT=http://localhost:9000
AWS_REGION=us-east-1
AWS_ACCESS_KEY=minioadmin
AWS_SECRET_KEY=minioadmin123
S3_PRIVATE_BUCKET_NAME=indeedhub-private
S3_PUBLIC_BUCKET_NAME=indeedhub-public
S3_PUBLIC_BUCKET_URL=http://localhost:7777/storage/
# BTCPay Server (Lightning payments)
BTCPAY_URL=https://your-btcpay.com
BTCPAY_STORE_ID=
BTCPAY_API_KEY=
BTCPAY_WEBHOOK_SECRET=
# Create a separate internal Lightning invoice with privateRouteHints.
# Only needed when BTCPay's built-in route hints are NOT enabled.
# If you enabled "Include hop hints" in BTCPay's Lightning settings,
# leave this as false — BTCPay handles route hints natively.
BTCPAY_ROUTE_HINTS=false
# User Pool - AWS Cognito
COGNITO_USER_POOL_ID=
COGNITO_CLIENT_ID=
# Nostr auth (required)
NOSTR_JWT_SECRET=generate_with_openssl_rand_hex_32
NOSTR_JWT_EXPIRES_IN=7d
# Sendgrid - Email Service
SENDGRID_API_KEY=
SENDGRID_SENDER=
SENDGRID_WAITLIST=
# Content encryption (required for transcoding)
AES_MASTER_SECRET=generate_64_hex_chars
# AWS KEYS
AWS_ACCESS_KEY=
AWS_SECRET_KEY=
AWS_REGION=
# AWS S3
S3_PUBLIC_BUCKET_URL=
S3_PUBLIC_BUCKET_NAME=
# AWS CloudFront
CLOUDFRONT_PRIVATE_KEY=
CLOUDFRONT_KEY_PAIR_ID=
# Note: Stripe has been removed. All payments are now via BTCPay Server (Lightning).
# PAY WITH FLASH - ENTHUSIAST SUBSCRIPTION
FLASH_JWT_SECRET_ENTHUSIAST=
# PAY WITH FLASH - FILM BUFF SUBSCRIPTION
FLASH_JWT_SECRET_FILM_BUFF=
# PAY WITH FLASH - CINEPHILE SUBSCRIPTION
FLASH_JWT_SECRET_CINEPHILE=
# PAY WITH FLASH - RSS ADDON SUBSCRIPTION
FLASH_JWT_SECRET_RSS_ADDON=
# PAY WITH FLASH - VERIFICATION ADDON SUBSCRIPTION
FLASH_JWT_SECRET_VERIFICATION_ADDON=
# Transcoding API
TRANSCODING_API_KEY=
TRANSCODING_API_URL=http://localhost:4001
# Podping - RSS Update Notifications
PODPING_URL=https://podping.cloud/
PODPING_KEY=
PODPING_USER_AGENT=
# Retool API KEY - Admin Dashboard
# Admin API (optional)
ADMIN_API_KEY=
# PostHog - Analytics
POSTHOG_API_KEY=
# Sentry - Error Tracking
SENTRY_ENVIRONMENT=
# BUYDRM
PRIVATE_AUTH_CERTIFICATE_KEY_ID =
DRM_SECRET_NAME =
# Project Review
DASHBOARD_REVIEW_URL = https://oneseventech.retool.com/apps/5e86de84-7cdb-11ef-998f-47951c6124a1/Testing%20IndeeHub/film-details?id=
PROJECT_REVIEW_RECIPIENT_EMAIL = <your email>

View File

@@ -49,6 +49,7 @@
"class-validator": "^0.14.1",
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.2.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.3",
"jwks-rsa": "^3.0.1",
"moment": "^2.30.1",
@@ -14299,6 +14300,15 @@
"node": ">= 0.4"
}
},
"node_modules/helmet": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
"integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",

View File

@@ -111,6 +111,7 @@
"class-validator": "^0.14.1",
"dayjs": "^1.11.13",
"fast-xml-parser": "^5.2.0",
"helmet": "^8.1.0",
"jsonwebtoken": "^9.0.3",
"jwks-rsa": "^3.0.1",
"moment": "^2.30.1",

View File

@@ -5,35 +5,62 @@ import {
Logger,
UnauthorizedException,
} from '@nestjs/common';
import { CognitoJwtVerifier } from 'aws-jwt-verify';
import { verify } from 'jsonwebtoken';
import { Socket } from 'socket.io';
interface NostrSessionPayload {
sub: string;
typ: 'nostr-session' | 'nostr-refresh';
uid?: string;
}
/**
* WebSocket JWT auth guard.
* Validates Nostr session JWTs (signed with NOSTR_JWT_SECRET).
* Replaces legacy Cognito-based validation.
*/
@Injectable()
export class WsJwtAuthGuard implements CanActivate {
static async validateToken(client: Socket) {
const authorization = this.extractTokenFromHeader(client.handshake);
if (!authorization) throw new UnauthorizedException();
const verifier = CognitoJwtVerifier.create({
userPoolId: process.env.COGNITO_USER_POOL_ID,
tokenUse: 'id',
clientId: process.env.COGNITO_CLIENT_ID,
});
await verifier.verify(authorization);
return true;
}
private static extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
async canActivate(context: ExecutionContext) {
static async validateToken(client: Socket): Promise<boolean> {
const token = WsJwtAuthGuard.extractTokenFromHeader(client.handshake);
if (!token) throw new UnauthorizedException('No token provided');
const secret = process.env.NOSTR_JWT_SECRET;
if (!secret) {
Logger.error('NOSTR_JWT_SECRET not configured', 'WsJwtAuthGuard');
throw new UnauthorizedException('Server misconfiguration');
}
try {
const payload = verify(token, secret) as NostrSessionPayload;
if (payload.typ !== 'nostr-session') {
throw new UnauthorizedException('Invalid token type');
}
(client as Socket & { data?: { pubkey?: string } }).data = {
pubkey: payload.sub,
};
return true;
} catch (error) {
if (error instanceof UnauthorizedException) throw error;
Logger.warn(`WebSocket token validation failed: ${error?.message}`);
throw new UnauthorizedException('Invalid or expired token');
}
}
async canActivate(context: ExecutionContext): Promise<boolean> {
if (context.getType() !== 'ws') return true;
const client: Socket = context.switchToWs().getClient();
try {
await WsJwtAuthGuard.validateToken(client);
return true;
} catch (error) {
Logger.error(`Error validating token: ${error.message}`);
if (error instanceof UnauthorizedException) throw error;
Logger.warn(`WebSocket auth failed: ${error?.message}`);
throw new UnauthorizedException();
}
}

View File

@@ -38,24 +38,41 @@ interface TranscodeJobData {
drmMediaId?: string;
}
const isProduction = process.env.ENVIRONMENT === 'production';
function requireEnv(name: string): string {
const val = process.env[name];
if (isProduction && !val) {
throw new Error(`FFmpeg worker: ${name} is required in production`);
}
return val || '';
}
const s3 = new S3Client({
region: process.env.AWS_REGION || 'us-east-1',
endpoint: process.env.S3_ENDPOINT || 'http://minio:9000',
forcePathStyle: true,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY || 'minioadmin',
secretAccessKey: process.env.AWS_SECRET_KEY || 'minioadmin123',
accessKeyId: isProduction
? requireEnv('AWS_ACCESS_KEY')
: process.env.AWS_ACCESS_KEY || 'minioadmin',
secretAccessKey: isProduction
? requireEnv('AWS_SECRET_KEY')
: process.env.AWS_SECRET_KEY || 'minioadmin123',
},
});
const privateBucket = process.env.S3_PRIVATE_BUCKET_NAME || 'indeedhub-private';
const privateBucket =
process.env.S3_PRIVATE_BUCKET_NAME || 'indeedhub-private';
function getPgClient(): Client {
return new Client({
host: process.env.DATABASE_HOST || 'postgres',
port: Number(process.env.DATABASE_PORT || '5432'),
user: process.env.DATABASE_USER || 'indeedhub',
password: process.env.DATABASE_PASSWORD || 'indeedhub_dev_2026',
password: isProduction
? requireEnv('DATABASE_PASSWORD')
: process.env.DATABASE_PASSWORD || 'indeedhub_dev_2026',
database: process.env.DATABASE_NAME || 'indeedhub',
});
}

View File

@@ -8,6 +8,7 @@ import {
ExpressAdapter,
NestExpressApplication,
} from '@nestjs/platform-express';
import helmet from 'helmet';
import { RawBodyRequest } from './types/raw-body-request';
import { validateEnvironment } from './common/validate-env';
@@ -49,6 +50,8 @@ async function bootstrap() {
useContainer(app.select(AppModule), { fallbackOnErrors: true });
app.use(helmet());
if (
process.env.ENVIRONMENT === 'development' ||
process.env.ENVIRONMENT === 'local'
@@ -73,6 +76,7 @@ async function bootstrap() {
'https://www.indeehub.studio',
'https://app.indeehub.studio',
'https://bff.indeehub.studio',
'https://archipelago.indeehub.studio',
];
if (process.env.FRONTEND_URL) {

View File

@@ -17,11 +17,17 @@
import { Client } from 'pg';
import { randomUUID } from 'node:crypto';
const isProduction = process.env.ENVIRONMENT === 'production';
const dbPassword = process.env.DATABASE_PASSWORD;
if (isProduction && !dbPassword) {
throw new Error('DATABASE_PASSWORD is required when ENVIRONMENT=production');
}
const client = new Client({
host: process.env.DATABASE_HOST || 'localhost',
port: Number(process.env.DATABASE_PORT || '5432'),
user: process.env.DATABASE_USER || 'indeedhub',
password: process.env.DATABASE_PASSWORD || 'indeedhub_dev_2026',
password: dbPassword || 'indeedhub_dev_2026',
database: process.env.DATABASE_NAME || 'indeedhub',
});