This commit is contained in:
230
server/config/database.js
Normal file
230
server/config/database.js
Normal file
@@ -0,0 +1,230 @@
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user