195 lines
7.6 KiB
JavaScript
195 lines
7.6 KiB
JavaScript
'use strict';
|
|
const path = require('path');
|
|
const Database = require('better-sqlite3');
|
|
|
|
const DB_PATH = process.env.DB_PATH || path.join(__dirname, 'data', 'datahub.db');
|
|
const db = new Database(DB_PATH);
|
|
db.pragma('journal_mode = WAL');
|
|
db.pragma('foreign_keys = ON');
|
|
|
|
function init() {
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT UNIQUE NOT NULL,
|
|
email TEXT UNIQUE NOT NULL CHECK (length(trim(email)) > 0),
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT 'user', -- 'admin' | 'user'
|
|
mfa_enabled INTEGER NOT NULL DEFAULT 0,
|
|
mfa_secret TEXT,
|
|
avatar TEXT, -- stored filename of the user's picture
|
|
avatar_mime TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS folders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
parent_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
|
|
-- Date the material in this folder was recorded (YYYY-MM-DD).
|
|
-- "Legal Validity" is derived from it: recorded date + 30 days.
|
|
recorded_date TEXT,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS files (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL, -- display name
|
|
stored_name TEXT NOT NULL, -- name on disk
|
|
size INTEGER NOT NULL DEFAULT 0,
|
|
mime TEXT,
|
|
folder_id INTEGER REFERENCES folders(id) ON DELETE CASCADE,
|
|
uploaded_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS access_requests (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
target_type TEXT NOT NULL, -- 'file' | 'folder'
|
|
target_id INTEGER NOT NULL,
|
|
target_name TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | denied
|
|
decided_by INTEGER REFERENCES users(id),
|
|
-- How long the approval lasts ('24h' | '15d' | '30d' | 'forever')
|
|
-- and when it runs out. NULL expiry means it never expires.
|
|
access_duration TEXT,
|
|
access_expires_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
decided_at TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS email_templates (
|
|
key TEXT PRIMARY KEY, -- 'request_to_admin' | 'approval_to_user' | 'denial_to_user'
|
|
subject TEXT NOT NULL,
|
|
body_html TEXT NOT NULL,
|
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS brand_images (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
label TEXT NOT NULL,
|
|
stored_name TEXT NOT NULL,
|
|
mime TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS logs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
|
actor TEXT, -- username or 'system'
|
|
actor_id INTEGER,
|
|
action TEXT NOT NULL, -- e.g. FILE_UPLOAD
|
|
page TEXT, -- 'Data Management' | 'View Data' | ...
|
|
detail TEXT,
|
|
ip TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
|
|
-- Entries shown on the "Version Control" page (managed from Data Management)
|
|
CREATE TABLE IF NOT EXISTS version_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
version TEXT NOT NULL,
|
|
title TEXT,
|
|
released_on TEXT,
|
|
notes TEXT,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT
|
|
);
|
|
-- Entries shown on the "Legislation" page (managed from Data Management)
|
|
CREATE TABLE IF NOT EXISTS legislation_entries (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
reference TEXT,
|
|
title TEXT NOT NULL,
|
|
effective_date TEXT,
|
|
summary TEXT,
|
|
link_url TEXT,
|
|
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
created_by INTEGER REFERENCES users(id),
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
updated_at TEXT
|
|
);
|
|
|
|
-- Every message the system sends (and any inbound message that is
|
|
-- recorded), independent of the general audit log.
|
|
CREATE TABLE IF NOT EXISTS mail_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ts TEXT NOT NULL DEFAULT (datetime('now')),
|
|
direction TEXT NOT NULL DEFAULT 'sent', -- 'sent' | 'received'
|
|
status TEXT NOT NULL, -- 'delivered' | 'queued' | 'failed'
|
|
from_addr TEXT,
|
|
to_addr TEXT,
|
|
subject TEXT,
|
|
body_preview TEXT,
|
|
attachments TEXT,
|
|
error TEXT,
|
|
context TEXT,
|
|
actor TEXT
|
|
);
|
|
`);
|
|
}
|
|
|
|
/**
|
|
* Add columns introduced after a database was first created.
|
|
* SQLite's ALTER TABLE ADD COLUMN is safe and non-destructive: existing rows
|
|
* simply get NULL for the new column.
|
|
*/
|
|
function migrate() {
|
|
const folderCols = db.prepare('PRAGMA table_info(folders)').all().map((c) => c.name);
|
|
if (!folderCols.includes('recorded_date')) {
|
|
db.exec('ALTER TABLE folders ADD COLUMN recorded_date TEXT');
|
|
console.log('[Infosec] Added folders.recorded_date (existing folders have no date yet).');
|
|
}
|
|
|
|
const userCols = db.prepare('PRAGMA table_info(users)').all().map((c) => c.name);
|
|
if (!userCols.includes('avatar')) {
|
|
db.exec('ALTER TABLE users ADD COLUMN avatar TEXT');
|
|
db.exec('ALTER TABLE users ADD COLUMN avatar_mime TEXT');
|
|
console.log('[Infosec] Added users.avatar (existing users have no picture yet).');
|
|
}
|
|
|
|
const reqCols = db.prepare('PRAGMA table_info(access_requests)').all().map((c) => c.name);
|
|
if (!reqCols.includes('access_duration')) {
|
|
db.exec("ALTER TABLE access_requests ADD COLUMN access_duration TEXT");
|
|
// Approvals granted before this release had no expiry, so they are
|
|
// permanent. Marking them explicitly keeps existing access working.
|
|
db.exec("UPDATE access_requests SET access_duration = 'forever' WHERE status = 'approved'");
|
|
console.log('[Infosec] Added access_requests.access_duration (existing approvals kept as permanent).');
|
|
}
|
|
if (!reqCols.includes('access_expires_at')) {
|
|
db.exec('ALTER TABLE access_requests ADD COLUMN access_expires_at TEXT');
|
|
console.log('[Infosec] Added access_requests.access_expires_at.');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Accounts with no email address cannot receive approval notifications.
|
|
* New databases block this outright (CHECK constraint above); databases created
|
|
* by earlier releases keep their original schema, so report any offenders at
|
|
* startup instead of failing silently.
|
|
*/
|
|
function reportMissingEmails() {
|
|
try {
|
|
const rows = db.prepare(
|
|
"SELECT username FROM users WHERE email IS NULL OR length(trim(email)) = 0"
|
|
).all();
|
|
if (rows.length) {
|
|
console.warn(
|
|
`[Infosec] ${rows.length} account(s) have no email address: ` +
|
|
`${rows.map((r) => r.username).join(', ')}. ` +
|
|
'Email is now mandatory — set one in Data Management → Users.'
|
|
);
|
|
}
|
|
return rows.length;
|
|
} catch (_) { return 0; }
|
|
}
|
|
|
|
module.exports = { db, init, migrate, DB_PATH, reportMissingEmails };
|