548 lines
23 KiB
JavaScript
548 lines
23 KiB
JavaScript
const Database = require('better-sqlite3-multiple-ciphers');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const bcrypt = require('bcryptjs');
|
|
|
|
const DB_PATH = process.env.DB_PATH || '/app/data/jama.db';
|
|
const DB_KEY = process.env.DB_KEY || '';
|
|
|
|
let db;
|
|
|
|
function getDb() {
|
|
if (!db) {
|
|
// Ensure the data directory exists before opening the DB
|
|
const dir = path.dirname(DB_PATH);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
console.log(`[DB] Created data directory: ${dir}`);
|
|
}
|
|
db = new Database(DB_PATH);
|
|
if (DB_KEY) {
|
|
// Use SQLCipher4 AES-256-CBC — compatible with standard sqlcipher CLI and DB Browser
|
|
// Must be applied before any other DB access
|
|
const safeKey = DB_KEY.replace(/'/g, "''");
|
|
db.pragma(`cipher='sqlcipher'`);
|
|
db.pragma(`legacy=4`);
|
|
db.pragma(`key='${safeKey}'`);
|
|
console.log('[DB] Encryption key applied (SQLCipher4)');
|
|
} else {
|
|
console.warn('[DB] WARNING: DB_KEY not set — database is unencrypted');
|
|
}
|
|
const journalMode = db.pragma('journal_mode = WAL', { simple: true });
|
|
if (journalMode !== 'wal') {
|
|
console.warn(`[DB] WARNING: journal_mode is '${journalMode}', expected 'wal' — performance may be degraded`);
|
|
}
|
|
db.pragma('synchronous = NORMAL'); // safe with WAL, faster than FULL
|
|
db.pragma('cache_size = -8000'); // 8MB page cache
|
|
db.pragma('foreign_keys = ON');
|
|
console.log(`[DB] Opened database at ${DB_PATH} (journal=${journalMode})`);
|
|
}
|
|
return db;
|
|
}
|
|
|
|
function initDb() {
|
|
const db = getDb();
|
|
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
email TEXT UNIQUE NOT NULL,
|
|
password TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'member',
|
|
status TEXT NOT NULL DEFAULT 'active',
|
|
is_default_admin INTEGER NOT NULL DEFAULT 0,
|
|
must_change_password INTEGER NOT NULL DEFAULT 1,
|
|
avatar TEXT,
|
|
about_me TEXT,
|
|
display_name TEXT,
|
|
hide_admin_tag INTEGER NOT NULL DEFAULT 0,
|
|
allow_dm INTEGER NOT NULL DEFAULT 1,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL DEFAULT 'public',
|
|
owner_id INTEGER,
|
|
is_default INTEGER NOT NULL DEFAULT 0,
|
|
is_readonly INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (owner_id) REFERENCES users(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS group_members (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
group_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(group_id, user_id),
|
|
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
group_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
content TEXT,
|
|
type TEXT NOT NULL DEFAULT 'text',
|
|
image_url TEXT,
|
|
reply_to_id INTEGER,
|
|
is_deleted INTEGER NOT NULL DEFAULT 0,
|
|
link_preview TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
|
FOREIGN KEY (reply_to_id) REFERENCES messages(id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS reactions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
message_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
emoji TEXT NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(message_id, user_id, emoji),
|
|
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS notifications (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
type TEXT NOT NULL,
|
|
message_id INTEGER,
|
|
group_id INTEGER,
|
|
from_user_id INTEGER,
|
|
is_read INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
id TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
expires_at TEXT NOT NULL,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS active_sessions (
|
|
user_id INTEGER NOT NULL,
|
|
device TEXT NOT NULL DEFAULT 'desktop',
|
|
token TEXT NOT NULL,
|
|
ua TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (user_id, device),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS push_subscriptions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL,
|
|
endpoint TEXT NOT NULL,
|
|
p256dh TEXT NOT NULL,
|
|
auth TEXT NOT NULL,
|
|
device TEXT NOT NULL DEFAULT 'desktop',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
UNIQUE(user_id, device),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- User groups (admin-managed, separate from chat groups)
|
|
CREATE TABLE IF NOT EXISTS user_groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
dm_group_id INTEGER,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (dm_group_id) REFERENCES groups(id) ON DELETE SET NULL
|
|
);
|
|
|
|
-- Members of user groups
|
|
CREATE TABLE IF NOT EXISTS user_group_members (
|
|
user_group_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (user_group_id, user_id),
|
|
FOREIGN KEY (user_group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
|
|
-- Multi-group DMs: admin-created DMs whose members are user groups
|
|
CREATE TABLE IF NOT EXISTS multi_group_dms (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
dm_group_id INTEGER, -- paired private group in groups table
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (dm_group_id) REFERENCES groups(id) ON DELETE SET NULL
|
|
);
|
|
|
|
-- User groups that are members of a multi-group DM
|
|
CREATE TABLE IF NOT EXISTS multi_group_dm_members (
|
|
multi_group_dm_id INTEGER NOT NULL,
|
|
user_group_id INTEGER NOT NULL,
|
|
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (multi_group_dm_id, user_group_id),
|
|
FOREIGN KEY (multi_group_dm_id) REFERENCES multi_group_dms(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_group_id) REFERENCES user_groups(id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
|
|
// Initialize default settings
|
|
const insertSetting = db.prepare('INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)');
|
|
insertSetting.run('app_name', process.env.APP_NAME || 'jama');
|
|
insertSetting.run('logo_url', '');
|
|
insertSetting.run('pw_reset_active', process.env.ADMPW_RESET === 'true' ? 'true' : 'false');
|
|
insertSetting.run('icon_newchat', '');
|
|
insertSetting.run('icon_groupinfo', '');
|
|
insertSetting.run('pwa_icon_192', '');
|
|
insertSetting.run('pwa_icon_512', '');
|
|
insertSetting.run('color_title', '');
|
|
insertSetting.run('color_title_dark', '');
|
|
insertSetting.run('color_avatar_public', '');
|
|
insertSetting.run('color_avatar_dm', '');
|
|
insertSetting.run('registration_code', '');
|
|
insertSetting.run('feature_branding', 'false');
|
|
insertSetting.run('feature_group_manager', 'false');
|
|
insertSetting.run('feature_schedule_manager', 'false');
|
|
insertSetting.run('app_type', 'JAMA-Chat');
|
|
insertSetting.run('team_group_managers', '');
|
|
insertSetting.run('team_schedule_managers', '');
|
|
insertSetting.run('team_tool_managers', '');
|
|
|
|
// Migration: add hide_admin_tag if upgrading from older version
|
|
try {
|
|
db.exec("ALTER TABLE users ADD COLUMN hide_admin_tag INTEGER NOT NULL DEFAULT 0");
|
|
console.log('[DB] Migration: added hide_admin_tag column');
|
|
} catch (e) { /* column already exists */ }
|
|
|
|
// Migration: add allow_dm if upgrading from older version
|
|
try {
|
|
db.exec("ALTER TABLE users ADD COLUMN allow_dm INTEGER NOT NULL DEFAULT 1");
|
|
console.log('[DB] Migration: added allow_dm column');
|
|
} catch (e) { /* column already exists */ }
|
|
|
|
// Migration: replace single-session active_sessions with per-device version
|
|
try {
|
|
const cols = db.prepare("PRAGMA table_info(active_sessions)").all().map(c => c.name);
|
|
if (!cols.includes('device')) {
|
|
db.exec("DROP TABLE IF EXISTS active_sessions");
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS active_sessions (
|
|
user_id INTEGER NOT NULL,
|
|
device TEXT NOT NULL DEFAULT 'desktop',
|
|
token TEXT NOT NULL,
|
|
ua TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (user_id, device),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
console.log('[DB] Migration: rebuilt active_sessions for per-device sessions');
|
|
}
|
|
} catch (e) { console.error('[DB] active_sessions migration error:', e.message); }
|
|
|
|
// Migration: add is_direct for user-to-user direct messages
|
|
try {
|
|
db.exec("ALTER TABLE groups ADD COLUMN is_direct INTEGER NOT NULL DEFAULT 0");
|
|
console.log('[DB] Migration: added is_direct column');
|
|
} catch (e) { /* column already exists */ }
|
|
|
|
// Migration: store both peer IDs so direct-message names survive member leave
|
|
try {
|
|
db.exec("ALTER TABLE groups ADD COLUMN direct_peer1_id INTEGER");
|
|
console.log('[DB] Migration: added direct_peer1_id column');
|
|
} catch (e) { /* column already exists */ }
|
|
try {
|
|
db.exec("ALTER TABLE groups ADD COLUMN direct_peer2_id INTEGER");
|
|
console.log('[DB] Migration: added direct_peer2_id column');
|
|
} catch (e) { /* column already exists */ }
|
|
|
|
// Migration: last_online timestamp per user
|
|
try {
|
|
db.exec("ALTER TABLE users ADD COLUMN last_online TEXT");
|
|
console.log('[DB] Migration: added last_online column');
|
|
} catch (e) { /* column already exists */ }
|
|
|
|
// Migration: help_dismissed preference per user
|
|
try {
|
|
db.exec("ALTER TABLE users ADD COLUMN help_dismissed INTEGER NOT NULL DEFAULT 0");
|
|
console.log('[DB] Migration: added help_dismissed column');
|
|
} catch (e) { /* column already exists */ }
|
|
|
|
// Migration: user-customised group display names (per-user, per-group)
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS user_group_names (
|
|
user_id INTEGER NOT NULL,
|
|
group_id INTEGER NOT NULL,
|
|
name TEXT NOT NULL,
|
|
PRIMARY KEY (user_id, group_id)
|
|
)
|
|
`);
|
|
console.log('[DB] Migration: user_group_names table ready');
|
|
} catch (e) { console.error('[DB] user_group_names migration error:', e.message); }
|
|
|
|
// Migration: pinned conversations (per-user, pins a group to top of sidebar)
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS pinned_conversations (
|
|
user_id INTEGER NOT NULL,
|
|
group_id INTEGER NOT NULL,
|
|
pinned_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (user_id, group_id),
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
console.log('[DB] Migration: pinned_conversations table ready');
|
|
} catch (e) { console.error('[DB] pinned_conversations migration error:', e.message); }
|
|
|
|
// Migration: is_managed flag on groups (admin-managed DMs via Group Manager)
|
|
try {
|
|
db.exec("ALTER TABLE groups ADD COLUMN is_managed INTEGER NOT NULL DEFAULT 0");
|
|
console.log('[DB] Migration: added is_managed column to groups');
|
|
} catch (e) { /* already exists */ }
|
|
|
|
// Migration: is_multi_group flag — distinguishes multi-group DMs from user-group DMs
|
|
try {
|
|
db.exec("ALTER TABLE groups ADD COLUMN is_multi_group INTEGER NOT NULL DEFAULT 0");
|
|
console.log('[DB] Migration: added is_multi_group column to groups');
|
|
} catch (e) { /* already exists */ }
|
|
// Back-fill feature_schedule_manager for installs that registered before this setting existed
|
|
try {
|
|
const appType = db.prepare("SELECT value FROM settings WHERE key = 'app_type'").get();
|
|
if (appType && appType.value === 'JAMA-Team') {
|
|
db.prepare("INSERT INTO settings (key, value) VALUES ('feature_schedule_manager', 'true') ON CONFLICT(key) DO UPDATE SET value = 'true' WHERE value = 'false'").run();
|
|
}
|
|
} catch(e) {}
|
|
|
|
// Back-fill is_multi_group for any existing multi-group DM groups
|
|
try {
|
|
db.exec("UPDATE groups SET is_multi_group = 1 WHERE id IN (SELECT dm_group_id FROM multi_group_dms WHERE dm_group_id IS NOT NULL)");
|
|
} catch (e) { /* ignore */ }
|
|
|
|
// Migration: user_groups and user_group_members tables
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS user_groups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
dm_group_id INTEGER,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (dm_group_id) REFERENCES groups(id) ON DELETE SET NULL
|
|
)
|
|
`);
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS user_group_members (
|
|
user_group_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (user_group_id, user_id),
|
|
FOREIGN KEY (user_group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS multi_group_dms (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
dm_group_id INTEGER,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (dm_group_id) REFERENCES groups(id) ON DELETE SET NULL
|
|
)
|
|
`);
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS multi_group_dm_members (
|
|
multi_group_dm_id INTEGER NOT NULL,
|
|
user_group_id INTEGER NOT NULL,
|
|
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (multi_group_dm_id, user_group_id),
|
|
FOREIGN KEY (multi_group_dm_id) REFERENCES multi_group_dms(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_group_id) REFERENCES user_groups(id) ON DELETE CASCADE
|
|
)
|
|
`);
|
|
// Migration: add joined_at to user_group_members if missing
|
|
try { db.exec("ALTER TABLE user_group_members ADD COLUMN joined_at TEXT NOT NULL DEFAULT (datetime('now'))"); } catch(e) {}
|
|
console.log('[DB] Migration: user_groups tables ready');
|
|
} catch (e) { console.error('[DB] user_groups migration error:', e.message); }
|
|
|
|
// ── Schedule Manager ────────────────────────────────────────────────────────
|
|
try {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS event_types (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
colour TEXT NOT NULL DEFAULT '#6366f1',
|
|
default_user_group_id INTEGER,
|
|
default_duration_hrs REAL,
|
|
is_default INTEGER NOT NULL DEFAULT 0,
|
|
is_protected INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (default_user_group_id) REFERENCES user_groups(id) ON DELETE SET NULL
|
|
);
|
|
CREATE TABLE IF NOT EXISTS events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
event_type_id INTEGER,
|
|
start_at TEXT NOT NULL,
|
|
end_at TEXT NOT NULL,
|
|
all_day INTEGER NOT NULL DEFAULT 0,
|
|
location TEXT,
|
|
description TEXT,
|
|
is_public INTEGER NOT NULL DEFAULT 1,
|
|
track_availability INTEGER NOT NULL DEFAULT 0,
|
|
created_by INTEGER NOT NULL,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
FOREIGN KEY (event_type_id) REFERENCES event_types(id) ON DELETE SET NULL,
|
|
FOREIGN KEY (created_by) REFERENCES users(id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS event_user_groups (
|
|
event_id INTEGER NOT NULL,
|
|
user_group_id INTEGER NOT NULL,
|
|
PRIMARY KEY (event_id, user_group_id),
|
|
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_group_id) REFERENCES user_groups(id) ON DELETE CASCADE
|
|
);
|
|
CREATE TABLE IF NOT EXISTS event_availability (
|
|
event_id INTEGER NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
response TEXT NOT NULL CHECK(response IN ('going','maybe','not_going')),
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (event_id, user_id),
|
|
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
|
|
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
);
|
|
`);
|
|
db.prepare("INSERT OR IGNORE INTO event_types (name, colour, is_default, is_protected) VALUES ('Default', '#9ca3af', 1, 1)").run();
|
|
db.prepare("INSERT OR IGNORE INTO event_types (name, colour, is_protected, default_duration_hrs) VALUES ('Event', '#6366f1', 1, NULL)").run();
|
|
db.prepare("INSERT OR IGNORE INTO event_types (name, colour, default_duration_hrs) VALUES ('Game', '#22c55e', 3.0)").run();
|
|
db.prepare("INSERT OR IGNORE INTO event_types (name, colour, default_duration_hrs) VALUES ('Practice', '#f59e0b', 1.0)").run();
|
|
// Migration: add is_protected if missing
|
|
try { db.exec("ALTER TABLE event_types ADD COLUMN is_protected INTEGER NOT NULL DEFAULT 0"); } catch(e) {}
|
|
try { db.exec("ALTER TABLE event_types ADD COLUMN default_duration_hrs REAL"); } catch(e) {}
|
|
// Ensure built-in types are protected
|
|
db.prepare("UPDATE event_types SET is_protected = 1 WHERE name IN ('Default', 'Event')").run();
|
|
console.log('[DB] Schedule Manager tables ready');
|
|
} catch (e) { console.error('[DB] Schedule Manager migration error:', e.message); }
|
|
|
|
console.log('[DB] Schema initialized');
|
|
return db;
|
|
}
|
|
|
|
function seedAdmin() {
|
|
const db = getDb();
|
|
|
|
// Strip any surrounding quotes from env vars (common docker-compose mistake)
|
|
const adminEmail = (process.env.ADMIN_EMAIL || 'admin@jama.local').replace(/^["']|["']$/g, '').trim();
|
|
const adminName = (process.env.ADMIN_NAME || 'Admin User').replace(/^["']|["']$/g, '').trim();
|
|
const adminPass = (process.env.ADMIN_PASS || 'Admin@1234').replace(/^["']|["']$/g, '').trim();
|
|
const pwReset = process.env.ADMPW_RESET === 'true';
|
|
|
|
console.log(`[DB] Checking for default admin (${adminEmail})...`);
|
|
|
|
const existing = db.prepare('SELECT * FROM users WHERE is_default_admin = 1').get();
|
|
|
|
if (!existing) {
|
|
try {
|
|
const hash = bcrypt.hashSync(adminPass, 10);
|
|
const result = db.prepare(`
|
|
INSERT INTO users (name, email, password, role, status, is_default_admin, must_change_password)
|
|
VALUES (?, ?, ?, 'admin', 'active', 1, 1)
|
|
`).run(adminName, adminEmail, hash);
|
|
|
|
console.log(`[DB] Default admin created: ${adminEmail} (id=${result.lastInsertRowid})`);
|
|
|
|
// Create default public group
|
|
const groupResult = db.prepare(`
|
|
INSERT INTO groups (name, type, is_default, owner_id)
|
|
VALUES (?, 'public', 1, ?)
|
|
`).run(process.env.DEFCHAT_NAME || 'General Chat', result.lastInsertRowid);
|
|
|
|
// Add admin to default group
|
|
db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)')
|
|
.run(groupResult.lastInsertRowid, result.lastInsertRowid);
|
|
|
|
console.log(`[DB] Default group created: ${process.env.DEFCHAT_NAME || 'General Chat'}`);
|
|
seedSupportGroup();
|
|
} catch (err) {
|
|
console.error('[DB] ERROR creating default admin:', err.message);
|
|
}
|
|
return;
|
|
}
|
|
|
|
console.log(`[DB] Default admin already exists (id=${existing.id})`);
|
|
|
|
// Handle ADMPW_RESET
|
|
if (pwReset) {
|
|
const hash = bcrypt.hashSync(adminPass, 10);
|
|
db.prepare(`
|
|
UPDATE users SET password = ?, must_change_password = 1, updated_at = datetime('now')
|
|
WHERE is_default_admin = 1
|
|
`).run(hash);
|
|
db.prepare("UPDATE settings SET value = 'true', updated_at = datetime('now') WHERE key = 'pw_reset_active'").run();
|
|
console.log('[DB] Admin password reset via ADMPW_RESET=true');
|
|
} else {
|
|
db.prepare("UPDATE settings SET value = 'false', updated_at = datetime('now') WHERE key = 'pw_reset_active'").run();
|
|
}
|
|
}
|
|
|
|
function addUserToPublicGroups(userId) {
|
|
const db = getDb();
|
|
const publicGroups = db.prepare("SELECT id FROM groups WHERE type = 'public'").all();
|
|
const insert = db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)');
|
|
for (const g of publicGroups) {
|
|
insert.run(g.id, userId);
|
|
}
|
|
}
|
|
|
|
function seedSupportGroup() {
|
|
const db = getDb();
|
|
|
|
// Create the Support group if it doesn't exist
|
|
const existing = db.prepare("SELECT id FROM groups WHERE name = 'Support' AND type = 'private'").get();
|
|
if (existing) return existing.id;
|
|
|
|
const admin = db.prepare('SELECT id FROM users WHERE is_default_admin = 1').get();
|
|
if (!admin) return null;
|
|
|
|
const result = db.prepare(`
|
|
INSERT INTO groups (name, type, owner_id, is_default)
|
|
VALUES ('Support', 'private', ?, 0)
|
|
`).run(admin.id);
|
|
|
|
const groupId = result.lastInsertRowid;
|
|
|
|
// Add all current admins to the Support group
|
|
const admins = db.prepare("SELECT id FROM users WHERE role = 'admin' AND status = 'active'").all();
|
|
const insert = db.prepare('INSERT OR IGNORE INTO group_members (group_id, user_id) VALUES (?, ?)');
|
|
for (const a of admins) insert.run(groupId, a.id);
|
|
|
|
console.log('[DB] Support group created');
|
|
return groupId;
|
|
}
|
|
|
|
function getOrCreateSupportGroup() {
|
|
const db = getDb();
|
|
const group = db.prepare("SELECT id FROM groups WHERE name = 'Support' AND type = 'private'").get();
|
|
if (group) return group.id;
|
|
return seedSupportGroup();
|
|
}
|
|
|
|
module.exports = { getDb, initDb, seedAdmin, seedSupportGroup, getOrCreateSupportGroup, addUserToPublicGroups };
|