import bcrypt from 'bcryptjs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const DATABASE_URL = process.env.DATABASE_URL; const isPostgres = !!(DATABASE_URL && DATABASE_URL.startsWith('postgres')); let db; // Convert ? placeholders to $1, $2, ... for PostgreSQL function convertPlaceholders(sql) { let index = 0; return sql.replace(/\?/g, () => `$${++index}`); } // ── SQLite Adapter ────────────────────────────────────────────────────────── class SqliteAdapter { async init() { const { default: Database } = await import('better-sqlite3'); const dbPath = process.env.SQLITE_PATH || path.join(__dirname, '..', '..', 'redlight.db'); this.db = Database(dbPath); this.db.pragma('journal_mode = WAL'); this.db.pragma('foreign_keys = ON'); } async get(sql, params = []) { return this.db.prepare(sql).get(...params); } async all(sql, params = []) { return this.db.prepare(sql).all(...params); } async run(sql, params = []) { const result = this.db.prepare(sql).run(...params); return { lastInsertRowid: Number(result.lastInsertRowid), changes: result.changes }; } async exec(sql) { this.db.exec(sql); } async columnExists(table, column) { const columns = this.db.pragma(`table_info(${table})`); return !!columns.find(c => c.name === column); } close() { this.db.close(); } } // ── PostgreSQL Adapter ────────────────────────────────────────────────────── class PostgresAdapter { async init() { const pg = await import('pg'); // Parse int8 (bigint / COUNT) as JS number instead of string pg.default.types.setTypeParser(20, val => parseInt(val, 10)); this.pool = new pg.default.Pool({ connectionString: DATABASE_URL }); } async get(sql, params = []) { const result = await this.pool.query(convertPlaceholders(sql), params); return result.rows[0] || undefined; } async all(sql, params = []) { const result = await this.pool.query(convertPlaceholders(sql), params); return result.rows; } async run(sql, params = []) { let pgSql = convertPlaceholders(sql); const isInsert = /^\s*INSERT/i.test(pgSql); if (isInsert && !/RETURNING/i.test(pgSql)) { pgSql += ' RETURNING id'; } const result = await this.pool.query(pgSql, params); return { lastInsertRowid: isInsert ? result.rows[0]?.id : undefined, changes: result.rowCount, }; } async exec(sql) { await this.pool.query(sql); } async columnExists(table, column) { const result = await this.pool.query( 'SELECT column_name FROM information_schema.columns WHERE table_name = $1 AND column_name = $2', [table, column] ); return result.rows.length > 0; } close() { this.pool?.end(); } } // ── Public API ────────────────────────────────────────────────────────────── export function getDb() { if (!db) { throw new Error('Database not initialised – call initDatabase() first'); } return db; } export async function initDatabase() { // Create the right adapter if (isPostgres) { console.log('📦 Using PostgreSQL database'); db = new PostgresAdapter(); } else { console.log('📦 Using SQLite database'); db = new SqliteAdapter(); } await db.init(); // ── Schema creation ───────────────────────────────────────────────────── if (isPostgres) { await db.exec(` CREATE TABLE IF NOT EXISTS users ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')), language TEXT DEFAULT 'de', theme TEXT DEFAULT 'dark', avatar_color TEXT DEFAULT '#6366f1', avatar_image TEXT DEFAULT NULL, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS rooms ( id SERIAL PRIMARY KEY, uid TEXT UNIQUE NOT NULL, name TEXT NOT NULL, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, welcome_message TEXT DEFAULT 'Willkommen im Meeting!', max_participants INTEGER DEFAULT 0, access_code TEXT, mute_on_join INTEGER DEFAULT 1, require_approval INTEGER DEFAULT 0, anyone_can_start INTEGER DEFAULT 0, all_join_moderator INTEGER DEFAULT 0, record_meeting INTEGER DEFAULT 1, guest_access INTEGER DEFAULT 0, moderator_code TEXT, created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_rooms_user_id ON rooms(user_id); CREATE INDEX IF NOT EXISTS idx_rooms_uid ON rooms(uid); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); `); } else { await db.exec(` CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, email TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, role TEXT DEFAULT 'user' CHECK(role IN ('user', 'admin')), language TEXT DEFAULT 'de', theme TEXT DEFAULT 'dark', avatar_color TEXT DEFAULT '#6366f1', avatar_image TEXT DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS rooms ( id INTEGER PRIMARY KEY AUTOINCREMENT, uid TEXT UNIQUE NOT NULL, name TEXT NOT NULL, user_id INTEGER NOT NULL, welcome_message TEXT DEFAULT 'Willkommen im Meeting!', max_participants INTEGER DEFAULT 0, access_code TEXT, mute_on_join INTEGER DEFAULT 1, require_approval INTEGER DEFAULT 0, anyone_can_start INTEGER DEFAULT 0, all_join_moderator INTEGER DEFAULT 0, record_meeting INTEGER DEFAULT 1, guest_access INTEGER DEFAULT 0, moderator_code TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE ); CREATE INDEX IF NOT EXISTS idx_rooms_user_id ON rooms(user_id); CREATE INDEX IF NOT EXISTS idx_rooms_uid ON rooms(uid); CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); `); } // ── Migrations ────────────────────────────────────────────────────────── if (!(await db.columnExists('users', 'avatar_image'))) { await db.exec('ALTER TABLE users ADD COLUMN avatar_image TEXT DEFAULT NULL'); } if (!(await db.columnExists('rooms', 'guest_access'))) { await db.exec('ALTER TABLE rooms ADD COLUMN guest_access INTEGER DEFAULT 0'); } if (!(await db.columnExists('rooms', 'moderator_code'))) { await db.exec('ALTER TABLE rooms ADD COLUMN moderator_code TEXT'); } // ── Default admin ─────────────────────────────────────────────────────── const adminEmail = process.env.ADMIN_EMAIL || 'admin@example.com'; const adminPassword = process.env.ADMIN_PASSWORD || 'admin123'; const existingAdmin = await db.get('SELECT id FROM users WHERE email = ?', [adminEmail]); if (!existingAdmin) { const hash = bcrypt.hashSync(adminPassword, 12); await db.run( 'INSERT INTO users (name, email, password_hash, role) VALUES (?, ?, ?, ?)', ['Administrator', adminEmail, hash, 'admin'] ); console.log(`✅ Default admin created: ${adminEmail}`); } }