v_2.3
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
'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 };
|
||||
Reference in New Issue
Block a user