186 lines
5.6 KiB
JavaScript
186 lines
5.6 KiB
JavaScript
const Database = require('better-sqlite3')
|
|
const fs = require('fs')
|
|
const path = require('path')
|
|
const { ensureDecrypted } = require('./weekly_crypto')
|
|
|
|
let db = null
|
|
|
|
const getDB = () => {
|
|
if (!db) throw new Error('Database not initialized')
|
|
return db
|
|
}
|
|
|
|
const tryOpen = (dbPath) => {
|
|
try {
|
|
const d = new Database(dbPath)
|
|
try {
|
|
const row = d.prepare(`PRAGMA integrity_check`).get()
|
|
const ok = row && (row.integrity_check === 'ok' || row['integrity_check'] === 'ok' || Object.values(row)[0] === 'ok')
|
|
if (!ok) throw new Error('integrity_check_failed')
|
|
} catch (e) {
|
|
throw e
|
|
}
|
|
return d
|
|
} catch (e) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const initDB = (dbPath, sealPath) => {
|
|
if (db) return db
|
|
|
|
// Robust Open
|
|
db = tryOpen(dbPath)
|
|
if (!db) {
|
|
// Attempt recovery
|
|
if (fs.existsSync(dbPath)) {
|
|
const badName = `${dbPath}.bad-${Date.now()}`
|
|
try { fs.renameSync(dbPath, badName) } catch {}
|
|
console.warn(`Database corrupt, renamed to ${badName}`)
|
|
}
|
|
|
|
// Try decrypt from seal
|
|
if (sealPath) ensureDecrypted(dbPath, sealPath)
|
|
|
|
db = tryOpen(dbPath)
|
|
if (!db) {
|
|
// If still fails, create fresh (previous was renamed or didn't exist)
|
|
db = new Database(dbPath)
|
|
}
|
|
}
|
|
|
|
// Pragma
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('synchronous = NORMAL');
|
|
db.pragma('busy_timeout = 4000');
|
|
|
|
// Users
|
|
db.exec(`CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE,
|
|
pass_salt TEXT,
|
|
pass_hash TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
)`)
|
|
|
|
// Reports
|
|
db.exec(`CREATE TABLE IF NOT EXISTS reports (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
week_id TEXT UNIQUE,
|
|
title_enc TEXT, title_iv TEXT, title_tag TEXT,
|
|
summary_enc TEXT, summary_iv TEXT, summary_tag TEXT,
|
|
content_enc TEXT, content_iv TEXT, content_tag TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now'))
|
|
)`)
|
|
|
|
// Events
|
|
db.exec(`CREATE TABLE IF NOT EXISTS events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
report_id INTEGER,
|
|
parent_event_id INTEGER,
|
|
title_enc TEXT, title_iv TEXT, title_tag TEXT,
|
|
content_enc TEXT, content_iv TEXT, content_tag TEXT,
|
|
status_code TEXT,
|
|
status_enc TEXT, status_iv TEXT, status_tag TEXT,
|
|
deadline TEXT,
|
|
push_required INTEGER DEFAULT 0,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
updated_at TEXT DEFAULT (datetime('now')),
|
|
completed_at TEXT,
|
|
FOREIGN KEY(report_id) REFERENCES reports(id)
|
|
)`)
|
|
|
|
// Event Reviews
|
|
db.exec(`CREATE TABLE IF NOT EXISTS event_reviews (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
event_id INTEGER,
|
|
review_enc TEXT, review_iv TEXT, review_tag TEXT,
|
|
rating INTEGER,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
FOREIGN KEY(event_id) REFERENCES events(id)
|
|
)`)
|
|
|
|
// Attachments
|
|
db.exec(`CREATE TABLE IF NOT EXISTS attachments (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
event_id INTEGER,
|
|
filename_enc TEXT, filename_iv TEXT, filename_tag TEXT,
|
|
mime_enc TEXT, mime_iv TEXT, mime_tag TEXT,
|
|
size INTEGER,
|
|
chunks INTEGER DEFAULT 1,
|
|
store_enc TEXT, store_iv TEXT, store_tag TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
FOREIGN KEY(event_id) REFERENCES events(id)
|
|
)`)
|
|
|
|
// Versions
|
|
db.exec(`CREATE TABLE IF NOT EXISTS versions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
subject_type TEXT,
|
|
subject_id INTEGER,
|
|
snapshot_enc TEXT, snapshot_iv TEXT, snapshot_tag TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
)`)
|
|
|
|
// Logs
|
|
db.exec(`CREATE TABLE IF NOT EXISTS logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
action TEXT,
|
|
details TEXT,
|
|
source TEXT,
|
|
created_at TEXT DEFAULT (datetime('now'))
|
|
)`)
|
|
|
|
// Email Sends
|
|
db.exec(`CREATE TABLE IF NOT EXISTS email_sends (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
day TEXT,
|
|
report_id INTEGER,
|
|
event_id INTEGER,
|
|
category TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
UNIQUE(day, report_id, event_id, category)
|
|
)`)
|
|
|
|
// Event External (Ensure table exists for integrations)
|
|
db.exec(`CREATE TABLE IF NOT EXISTS event_external (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
source TEXT NOT NULL,
|
|
ext_id TEXT NOT NULL,
|
|
event_id INTEGER NOT NULL,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
UNIQUE(source, ext_id),
|
|
FOREIGN KEY(event_id) REFERENCES events(id)
|
|
);`)
|
|
|
|
// Event Relations
|
|
db.exec(`CREATE TABLE IF NOT EXISTS event_relations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
event_id INTEGER NOT NULL,
|
|
target_event_id INTEGER NOT NULL,
|
|
relation_type TEXT,
|
|
created_at TEXT DEFAULT (datetime('now')),
|
|
FOREIGN KEY(event_id) REFERENCES events(id),
|
|
FOREIGN KEY(target_event_id) REFERENCES events(id)
|
|
);`)
|
|
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_reports_week_id ON reports(week_id)`)
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_events_report_id ON events(report_id)`)
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_events_parent_event_id ON events(parent_event_id)`)
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_event_reviews_event_id ON event_reviews(event_id)`)
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_attachments_event_id ON attachments(event_id)`)
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_versions_subject ON versions(subject_type, subject_id)`)
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_event_external_event_id ON event_external(event_id)`)
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_event_relations_event_id ON event_relations(event_id)`)
|
|
db.exec(`CREATE INDEX IF NOT EXISTS idx_event_relations_target_event_id ON event_relations(target_event_id)`)
|
|
|
|
return db
|
|
}
|
|
|
|
module.exports = {
|
|
initDB,
|
|
getDB
|
|
}
|