diff --git a/backend/src/db.ts b/backend/src/db.ts index 6b21c96..d28e464 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -1,32 +1,89 @@ -import { Pool } from 'pg'; -import { readFile } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import bcrypt from 'bcryptjs'; -import { config } from './config.js'; +import pg from 'pg'; -export const pool = new Pool({ connectionString: config.databaseUrl }); +const { Pool } = pg; -const __dirname = dirname(fileURLToPath(import.meta.url)); +export const pool = new Pool({ + connectionString: process.env.DATABASE_URL, +}); -export async function initDb(): Promise { - const migration = await readFile(join(__dirname, '../migrations/001_init.sql'), 'utf8').catch(async () => { - return readFile(join(process.cwd(), 'migrations/001_init.sql'), 'utf8'); - }); - await pool.query(migration); - - await pool.query( - `INSERT INTO nodes(name, hostname, is_current) - VALUES($1, $2, true) - ON CONFLICT (name) DO UPDATE SET hostname = EXCLUDED.hostname, is_current = true, updated_at = now()`, - [config.nodeName, config.nodeHostname], - ); - - const hash = await bcrypt.hash(config.adminPassword, 12); - await pool.query( - `INSERT INTO admin_users(email, password_hash, role) - VALUES($1, $2, 'super_admin') - ON CONFLICT (email) DO NOTHING`, - [config.adminEmail.toLowerCase(), hash], - ); +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); } + +export async function initDb() { + const maxAttempts = 30; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + await pool.query('SELECT 1'); + console.log(`✓ PostgreSQL connected`); + + await pool.query(` + CREATE TABLE IF NOT EXISTS nodes ( + id SERIAL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + hostname TEXT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now() + ); + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS domains ( + id SERIAL PRIMARY KEY, + domain TEXT UNIQUE NOT NULL, + node_name TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active', + first_seen_at TIMESTAMPTZ DEFAULT now(), + last_seen_at TIMESTAMPTZ DEFAULT now() + ); + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS mailboxes ( + id SERIAL PRIMARY KEY, + email_address TEXT UNIQUE NOT NULL, + domain TEXT NOT NULL, + node_name TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'active', + used_bytes BIGINT DEFAULT 0, + last_usage_scan_at TIMESTAMPTZ, + first_seen_at TIMESTAMPTZ DEFAULT now(), + last_seen_at TIMESTAMPTZ DEFAULT now() + ); + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS admin_users ( + id SERIAL PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + role TEXT NOT NULL DEFAULT 'super_admin', + created_at TIMESTAMPTZ DEFAULT now() + ); + `); + + await pool.query(` + CREATE TABLE IF NOT EXISTS audit_log ( + id SERIAL PRIMARY KEY, + actor_email TEXT, + action TEXT NOT NULL, + target TEXT, + details JSONB, + created_at TIMESTAMPTZ DEFAULT now() + ); + `); + + return; + } catch (err: any) { + console.warn( + `PostgreSQL not ready yet (${attempt}/${maxAttempts}): ${err.message}` + ); + + if (attempt === maxAttempts) { + throw err; + } + + await sleep(2000); + } + } +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 90a9c9b..64e944e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,15 +2,19 @@ services: mailadmin-db: image: postgres:16 container_name: mailadmin-db - restart: unless-stopped environment: POSTGRES_DB: mailadmin POSTGRES_USER: mailadmin - POSTGRES_PASSWORD: ${MAILADMIN_DB_PASSWORD:-change-me} + POSTGRES_PASSWORD: ${MAILADMIN_DB_PASSWORD} volumes: - - ./data/postgres:/var/lib/postgresql/data - networks: - - mail_network + - mailadmin-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mailadmin -d mailadmin"] + interval: 5s + timeout: 5s + retries: 20 + start_period: 10s + restart: unless-stopped mailadmin: build: @@ -19,7 +23,8 @@ services: container_name: mailadmin restart: unless-stopped depends_on: - - mailadmin-db + mailadmin-db: + condition: service_healthy environment: NODE_ENV: production PORT: 3000