This commit is contained in:
2026-04-26 14:11:53 -05:00
parent 844c63dd85
commit 9b3b99b38a
2 changed files with 96 additions and 34 deletions

View File

@@ -1,32 +1,89 @@
import { Pool } from 'pg'; import pg 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';
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<void> {
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( function sleep(ms: number) {
`INSERT INTO nodes(name, hostname, is_current) return new Promise((resolve) => setTimeout(resolve, ms));
VALUES($1, $2, true) }
ON CONFLICT (name) DO UPDATE SET hostname = EXCLUDED.hostname, is_current = true, updated_at = now()`,
[config.nodeName, config.nodeHostname], export async function initDb() {
); const maxAttempts = 30;
const hash = await bcrypt.hash(config.adminPassword, 12); for (let attempt = 1; attempt <= maxAttempts; attempt++) {
await pool.query( try {
`INSERT INTO admin_users(email, password_hash, role) await pool.query('SELECT 1');
VALUES($1, $2, 'super_admin') console.log(`✓ PostgreSQL connected`);
ON CONFLICT (email) DO NOTHING`,
[config.adminEmail.toLowerCase(), hash], 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);
}
}
} }

View File

@@ -2,15 +2,19 @@ services:
mailadmin-db: mailadmin-db:
image: postgres:16 image: postgres:16
container_name: mailadmin-db container_name: mailadmin-db
restart: unless-stopped
environment: environment:
POSTGRES_DB: mailadmin POSTGRES_DB: mailadmin
POSTGRES_USER: mailadmin POSTGRES_USER: mailadmin
POSTGRES_PASSWORD: ${MAILADMIN_DB_PASSWORD:-change-me} POSTGRES_PASSWORD: ${MAILADMIN_DB_PASSWORD}
volumes: volumes:
- ./data/postgres:/var/lib/postgresql/data - mailadmin-db-data:/var/lib/postgresql/data
networks: healthcheck:
- mail_network test: ["CMD-SHELL", "pg_isready -U mailadmin -d mailadmin"]
interval: 5s
timeout: 5s
retries: 20
start_period: 10s
restart: unless-stopped
mailadmin: mailadmin:
build: build:
@@ -19,7 +23,8 @@ services:
container_name: mailadmin container_name: mailadmin
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- mailadmin-db mailadmin-db:
condition: service_healthy
environment: environment:
NODE_ENV: production NODE_ENV: production
PORT: 3000 PORT: 3000