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

@@ -0,0 +1,305 @@
/**
* Database Seed Script
*
* 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
*
* Run: node dist/scripts/seed-content.js
* Requires: DATABASE_HOST, DATABASE_PORT, DATABASE_USER, etc. in env
*/
import { Client } from 'pg';
import { randomUUID } from 'node:crypto';
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',
database: process.env.DATABASE_NAME || 'indeedhub',
});
// ── Test Users ────────────────────────────────────────────────
// Using the same dev personas from the frontend seed
const testUsers = [
{
id: randomUUID(),
email: 'alice@indeedhub.local',
legalName: 'Alice Developer',
nostrPubkey:
'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2',
},
{
id: randomUUID(),
email: 'bob@indeedhub.local',
legalName: 'Bob Filmmaker',
nostrPubkey:
'b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3',
},
{
id: randomUUID(),
email: 'charlie@indeedhub.local',
legalName: 'Charlie Audience',
nostrPubkey:
'c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4',
},
];
// ── Genres ────────────────────────────────────────────────────
const genres = [
{ id: randomUUID(), name: 'Documentary', type: 'film' },
{ id: randomUUID(), name: 'Drama', type: 'film' },
{ id: randomUUID(), name: 'Action', type: 'film' },
{ id: randomUUID(), name: 'Horror', type: 'film' },
{ id: randomUUID(), name: 'Comedy', type: 'film' },
{ id: randomUUID(), name: 'Thriller', type: 'film' },
{ id: randomUUID(), name: 'Science Fiction', type: 'film' },
{ id: randomUUID(), name: 'Animation', type: 'film' },
];
// ── IndeeHub Films ────────────────────────────────────────────
const indeeHubFilms = [
{
id: 'god-bless-bitcoin',
title: 'God Bless Bitcoin',
synopsis:
'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',
},
{
id: 'thethingswecarry',
title: 'The Things We Carry',
synopsis:
'A compelling narrative exploring the emotional weight of our past.',
poster: '/images/films/posters/thethingswecarry.webp',
genre: 'Drama',
categories: ['Drama'],
deliveryMode: 'native',
},
{
id: 'duel',
title: 'Duel',
synopsis: 'An intense confrontation that tests the limits of human resolve.',
poster: '/images/films/posters/duel.png',
genre: 'Action',
categories: ['Drama', 'Action'],
deliveryMode: 'native',
},
{
id: '2b0d7349-c010-47a0-b584-49e1bf86ab2f',
title: 'Hard Money',
synopsis:
'Understanding sound money principles and monetary sovereignty.',
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',
},
];
async function seed() {
console.log('[seed] Connecting to database...');
await client.connect();
try {
// Run inside a transaction
await client.query('BEGIN');
// 1. Seed genres
console.log('[seed] Seeding genres...');
for (const genre of genres) {
await client.query(
`INSERT INTO genres (id, name, type, created_at, updated_at)
VALUES ($1, $2, $3, NOW(), NOW())
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name`,
[genre.id, genre.name, genre.type],
);
}
// Build genre lookup
const genreLookup: Record<string, string> = {};
const genreRows = await client.query('SELECT id, name FROM genres');
for (const row of genreRows.rows) {
genreLookup[row.name] = row.id;
}
// 2. Seed test users
console.log('[seed] Seeding test users...');
for (const user of testUsers) {
await client.query(
`INSERT INTO users (id, email, legal_name, nostr_pubkey, created_at, updated_at)
VALUES ($1, $2, $3, $4, NOW(), NOW())
ON CONFLICT (id) DO UPDATE SET
email = EXCLUDED.email,
nostr_pubkey = EXCLUDED.nostr_pubkey`,
[user.id, user.email, user.legalName, user.nostrPubkey],
);
}
// 3. Seed subscriptions for test users (cinephile plan)
console.log('[seed] Seeding subscriptions...');
for (const user of testUsers) {
const subId = randomUUID();
const periodEnd = new Date();
periodEnd.setFullYear(periodEnd.getFullYear() + 1);
await client.query(
`INSERT INTO subscriptions (id, user_id, type, period, status, period_end, created_at)
VALUES ($1, $2, 'cinephile', 'yearly', 'succeeded', $3, NOW())
ON CONFLICT DO NOTHING`,
[subId, user.id, periodEnd],
);
}
// 4. Seed IndeeHub films
console.log('[seed] Seeding IndeeHub films...');
for (const film of indeeHubFilms) {
const genreId = genreLookup[film.genre] || null;
await client.query(
`INSERT INTO projects (id, name, title, slug, synopsis, poster, status, type, genre_id, delivery_mode, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, 'published', 'film', $7, $8, NOW(), NOW())
ON CONFLICT (id) DO UPDATE SET
title = EXCLUDED.title,
synopsis = EXCLUDED.synopsis,
poster = EXCLUDED.poster,
delivery_mode = EXCLUDED.delivery_mode`,
[
film.id,
film.title,
film.title,
film.id,
film.synopsis,
film.poster,
genreId,
film.deliveryMode,
],
);
// Create a content record for the film
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())
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`,
[contentId, film.id, film.title, film.synopsis],
);
}
await client.query('COMMIT');
console.log('[seed] Database seeded successfully!');
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);
process.exit(1);
} finally {
await client.end();
}
}
seed().catch(console.error);