Files
redlight/server/config/database.js
Michelle 54d6ee553a
Some checks failed
Build & Push Docker Image / build (push) Failing after 53s
Init v1.0.0
2026-02-24 18:14:16 +01:00

231 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`);
}
}