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 { 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<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(
`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);
}
}
}

View File

@@ -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