Init v1.0.0
Some checks failed
Build & Push Docker Image / build (push) Failing after 53s

This commit is contained in:
2026-02-24 18:14:16 +01:00
commit 54d6ee553a
49 changed files with 10410 additions and 0 deletions

114
server/config/bbb.js Normal file
View File

@@ -0,0 +1,114 @@
import crypto from 'crypto';
import xml2js from 'xml2js';
const BBB_URL = process.env.BBB_URL || 'https://your-bbb-server.com/bigbluebutton/api/';
const BBB_SECRET = process.env.BBB_SECRET || '';
function getChecksum(apiCall, params) {
const queryString = new URLSearchParams(params).toString();
const raw = apiCall + queryString + BBB_SECRET;
return crypto.createHash('sha256').update(raw).digest('hex');
}
function buildUrl(apiCall, params = {}) {
const checksum = getChecksum(apiCall, params);
const queryString = new URLSearchParams({ ...params, checksum }).toString();
return `${BBB_URL}${apiCall}?${queryString}`;
}
async function apiCall(apiCallName, params = {}) {
const url = buildUrl(apiCallName, params);
try {
const response = await fetch(url);
const xml = await response.text();
const result = await xml2js.parseStringPromise(xml, {
explicitArray: false,
trim: true,
});
return result.response;
} catch (error) {
console.error(`BBB API error (${apiCallName}):`, error.message);
throw error;
}
}
// Generate deterministic passwords from room UID
function getRoomPasswords(uid) {
const modPw = crypto.createHash('sha256').update(uid + '_mod_' + BBB_SECRET).digest('hex').substring(0, 16);
const attPw = crypto.createHash('sha256').update(uid + '_att_' + BBB_SECRET).digest('hex').substring(0, 16);
return { moderatorPW: modPw, attendeePW: attPw };
}
export async function createMeeting(room) {
const { moderatorPW, attendeePW } = getRoomPasswords(room.uid);
const params = {
meetingID: room.uid,
name: room.name,
attendeePW,
moderatorPW,
welcome: room.welcome_message || 'Willkommen!',
record: room.record_meeting ? 'true' : 'false',
autoStartRecording: 'false',
allowStartStopRecording: 'true',
muteOnStart: room.mute_on_join ? 'true' : 'false',
'meta_bbb-origin': 'Redlight',
'meta_bbb-origin-server-name': 'Redlight',
};
if (room.max_participants > 0) {
params.maxParticipants = room.max_participants.toString();
}
if (room.access_code) {
params.lockSettingsLockOnJoin = 'true';
}
return apiCall('create', params);
}
export async function joinMeeting(uid, name, isModerator = false) {
const { moderatorPW, attendeePW } = getRoomPasswords(uid);
const params = {
meetingID: uid,
fullName: name,
password: isModerator ? moderatorPW : attendeePW,
redirect: 'true',
};
return buildUrl('join', params);
}
export async function endMeeting(uid) {
const { moderatorPW } = getRoomPasswords(uid);
return apiCall('end', { meetingID: uid, password: moderatorPW });
}
export async function getMeetingInfo(uid) {
return apiCall('getMeetingInfo', { meetingID: uid });
}
export async function isMeetingRunning(uid) {
const result = await apiCall('isMeetingRunning', { meetingID: uid });
return result.running === 'true';
}
export async function getMeetings() {
return apiCall('getMeetings', {});
}
export async function getRecordings(meetingID) {
const params = meetingID ? { meetingID } : {};
const result = await apiCall('getRecordings', params);
if (result.returncode !== 'SUCCESS' || !result.recordings) {
return [];
}
const recordings = result.recordings.recording;
if (!recordings) return [];
return Array.isArray(recordings) ? recordings : [recordings];
}
export async function deleteRecording(recordID) {
return apiCall('deleteRecordings', { recordID });
}
export async function publishRecording(recordID, publish) {
return apiCall('publishRecordings', { recordID, publish: publish ? 'true' : 'false' });
}
export { getRoomPasswords };

230
server/config/database.js Normal file
View 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}`);
}
}