Files
rosterchirp-dev/backend/src/models/db.js

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 };