Files

195 lines
7.6 KiB
JavaScript
Raw Permalink Normal View History

2026-09-13 20:12:28 +01:00
'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('[ISDSS] 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('[ISDSS] 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('[ISDSS] 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('[ISDSS] 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(
`[ISDSS] ${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 };