27 KiB
Infosec — v2.3
A self-hosted, secure data-access portal with an admin approval workflow. Built with Node.js + Express + SQLite. No external database or cloud service required.
© 2026 Martinhal IT - Joao Vaz - Version 2.3
What it does
| # | Requirement | Where it lives |
|---|---|---|
| 1 | Login page for users | public/login.html + routes/auth.js (first boot: public/setup.html + routes/setup.js) |
| 2 | Users can add MFA to their account | My Account page → routes/auth.js (TOTP via authenticator app) |
| 3 | "View Data" page listing data to Open (view in a new tab) or Download | public/view-data.html (folder contents hidden until access is granted) |
| + | "Calendar View" page: month grid marking days that have events (folder Incident Dates); click a day to list them | public/calendar-view.html (reuses /api/data/tree) |
| 4 | Admin-only "Data Management" page: create/rename/delete/move folders & files, upload | public/data-management.html + routes/manage.js |
| + | Data Creation tab (Data Management): build files from scratch — a text-and-image Document or an event Timeline with branches — saved into any folder | public/data-management.html + routes/datacreate.js |
| + | Admin user management (add the people who request data) | Data Management → Users tab → routes/users.js |
| 5 | Items created in Data Management appear on View Data | Same folder/file tables power both pages |
| 6 | Every access request emails all administrators that an approval is pending; approval/denial emails the requester | routes/data.js (/request, /requests/:id/approve) |
| 7 | All email text is configurable, including uploading & using images | Data Management → Email Templates & Brand Images tabs → routes/settings.js |
| + | Mail server configuration in the interface | Data Management → Email Server tab → routes/mail.js |
| + | Log of every message sent/received, exportable & emailable | Data Management → Email Log tab → routes/mail.js |
| 8 | "Logs" page recording every action on the other pages | public/logs.html + lib/audit.js |
| + | Admin "Storage" page: server disk plus per-folder usage | public/storage.html + routes/storage.js |
| 9 | Logs can be exported (CSV) and/or emailed | Logs page → routes/logs.js |
| 10 | Footer on every page after login: © 2026 Martinhal IT - Joao Vaz - Version 2.3 |
Rendered by public/js/app.js |
| + | Infosec logo in the sidebar of signed-in pages | public/assets/infosec-logo.png |
| + | Per-user profile picture, shown by the username | routes/auth.js avatar endpoints |
| + | Copy of every sent email placed in the mail server's Sent folder | lib/mailer.js (IMAP) |
| + | Admin-only "Version Control" page (release history), linked below Logs | public/version-control.html + routes/versions.js |
| + | "Legislation" page for all users, managed by admins | public/legislation.html + routes/legislation.js |
| + | Folder dates: Incident Date + derived Legal Validity | lib/dates.js, shown on View Data & Data Management |
Already running an earlier version? See UPGRADE.md — upgrading in place keeps all users, folders, files, approvals, and logs.
Run with Docker (recommended)
Everything runs in a single container. You only need Docker (and, for the one-command path, the Compose plugin).
Option A — Docker Compose
# 1. (Optional but recommended) set a session secret
cat > .env <<'ENV'
SESSION_SECRET=replace-with-a-long-random-string
# COOKIE_SECURE=true # when served over HTTPS
# SMTP_HOST=smtp.yourprovider.com
# SMTP_PORT=587
# SMTP_USER=...
# SMTP_PASS=...
# MAIL_FROM=Infosec <no-reply@yourdomain.com>
ENV
# 2. Build and start
docker compose up -d --build
Now open http://localhost:3000. On this first visit the site shows a setup wizard asking you to create the administrator account — no credentials in environment files. Fill it in and you are signed in straight away.
The database, uploaded files, and brand images persist in named volumes
(datahub-data, datahub-uploads, datahub-brand), so they survive restarts
and rebuilds.
docker compose logs -f # follow logs
docker compose down # stop (keeps volumes/data)
docker compose down -v # stop and DELETE all data (setup runs again)
Option B — plain Docker
docker build -t martinhal-datahub:1.3 .
docker run -d --name datahub -p 3000:3000 \
-e SESSION_SECRET="replace-with-a-long-random-string" \
-v datahub-data:/app/data \
-v datahub-uploads:/app/uploads \
-v datahub-brand:/app/brand \
martinhal-datahub:1.3
Notes:
- The administrator account is created through the browser on first boot, so no password ever needs to live in an env file, a shell history, or your compose file. Until it is created, every page redirects to the wizard and the APIs are locked; once created, the wizard can never be opened again.
- Set
COOKIE_SECURE=truewhen you put the container behind an HTTPS reverse proxy (nginx/Caddy/Traefik). - The image runs as a non-root user and includes a container
HEALTHCHECK.
Run without Docker
Requirements
- Node.js 18 or newer (tested on Node 22)
- npm
Setup
# 1. Install dependencies
npm install
# 2. Configure environment
cp .env.example .env
# → edit .env: set a long random SESSION_SECRET, and (optionally) your SMTP settings.
# 3. Prepare the database (creates tables + default email templates)
npm run init
# 4. Start the server
npm start
Then open http://localhost:3000 and the setup wizard will ask you to create the administrator account.
npm run initno longer asks for credentials — it only prepares the database, and it is safe to re-run at any time.
First boot
The very first time the site is opened it presents a one-step setup wizard:
- Choose an administrator username, email, and password (minimum 10 characters, with at least one letter and one number).
- The account is created and you are signed in immediately.
- The wizard closes permanently — from then on
/setup.htmljust redirects to the login page, and a second attempt to call the setup API is rejected.
While no administrator exists, every page redirects to the wizard and the rest
of the API returns 503 Setup required, so the instance cannot be used in a
half-configured state. Creating the account is recorded in the audit log as
SETUP_COMPLETED.
Right after setup, it's worth visiting My Account to switch on two-factor authentication for the administrator.
How access works
Access is granted per folder, and it cascades:
- A regular user browsing View Data sees only the names of top-level folders. The contents — sub-folders and files — stay hidden until access is granted, so folder names are the only thing disclosed up front.
- Requesting access to a folder notifies the administrators. Once approved, the user can open the folder and everything inside it: sub-folders, and all files, at any depth. No second request is ever needed for the contents.
- Loose files sitting at the root (not inside any folder) are still requested individually, since they have no folder to inherit from.
- Administrators see and download everything without requesting.
The server enforces this independently of the interface: the tree endpoint
filters out anything the user may not see. Files can be opened in a new tab
(rendered in place) as well as downloaded; both the view and download routes
re-check folder access on every request, and the view route additionally serves
the file hard-sandboxed (Content-Security-Policy: sandbox, nosniff) so an
opened file cannot run scripts or reach the app session.
Calendar View
Calendar View (in the left menu, next to View Data — available to every signed-in user) gives a month-by-month overview of which days have events. An event is a folder's Incident Date, so the calendar highlights each day that has one or more folders recorded on it and shows a count. Clicking a highlighted day lists those folders with their incident date, Legal Validity (marked when expired), and access state; folders you can open link through to View Data. The page reuses the same access-filtered /api/data/tree endpoint as View Data, so a user only ever sees the events they are allowed to see, and administrators see them all. No data is stored specifically for the calendar — it is derived from the folders that already exist.
Data Creation
The Data Management page has a Data Creation tab (admin only) for producing files without uploading them. Two kinds are supported:
- Document — an ordered mix of free-text blocks and images (with optional captions). Images are embedded directly into the output.
- Timeline — a list of main events, each with Name, Date, Time and Additional Data, plus optional branches off any event that carry the same four fields.
You pick a destination folder and a file name, optionally Preview the result, then Save. The output is written as a single self-contained .html file (no scripts) and stored through the same files table and uploads/ directory as uploaded files — so a created file appears in its folder and behaves like any other file on View Data, subject to the usual access control. Creation is audited as FILE_CREATE.
Version Control page
Administrators get a Version Control entry in the sidebar's Administration
section (directly below Logs) showing the release history. The page and its
data are admin-only. Entries are managed from
Data Management → Version Control, where entries can be added, edited, and
deleted. Each entry has a version, an optional title, release date
(YYYY-MM-DD), free-text notes, and a display-order number (higher appears
first). All three actions are audited (VERSION_ENTRY_CREATED,
VERSION_ENTRY_UPDATED, VERSION_ENTRY_DELETED).
Profile pictures
Each user can set a profile picture from My Account → Profile picture (PNG, JPEG, WebP or GIF, up to 4 MB). It appears next to their name at the bottom-left of the sidebar on every signed-in page, and updates there immediately without a reload. Users with no picture show their initials on a disc instead.
Pictures are stored under the avatars/ directory (a persistent Docker volume,
AVATAR_DIR), served only to signed-in users, and one picture per user: uploading
a replacement deletes the previous file, and removing it deletes the file too.
Uploads and removals are audited (AVATAR_UPDATED, AVATAR_REMOVED).
The avatar / avatar_mime columns are added to existing databases
automatically on the first start after upgrading.
Uploading files into a folder shows a progress bar while the transfer runs, and the dialog stays open until it completes.
Copying sent mail to the Sent folder
When the platform sends an email it can also drop a copy into the mail server's
Sent folder over IMAP, so messages the system sends appear alongside those
sent by a person. Configure it under Data Management → Email server in the
IMAP section: host, port, security, username, password, and optionally an exact
Sent-folder name (left blank, the server's own Sent mailbox is detected, falling
back to common names like Sent, Sent Items, INBOX.Sent,
[Gmail]/Sent Mail). The IMAP username and password default to the SMTP ones,
which is the usual case, and Test IMAP confirms the connection and which
folder will be used.
This is entirely optional: leave the IMAP host blank and nothing changes.
Crucially, filing the copy is best-effort — if IMAP is unreachable or misconfigured
the email is still sent normally, and only a MAIL_SENT_COPY_FAILED note is
written to the log. A failed copy never turns a delivered message into an error.
Branding
Signed-in pages show the Infosec logo at the top of the sidebar, from
public/assets/infosec-logo.png. Because the artwork is dark-on-white and the
sidebar is dark, it sits on a light rounded panel so it stays legible. The logo
already contains the Infosec wordmark, so it replaces the previous badge and text
rather than sitting beside a duplicate label.
The login and first-run setup screens are deliberately left as they were, with the original monogram.
To change the logo, replace public/assets/infosec-logo.png (a wide landscape
image around 3:1 works best; it is displayed about 184px wide, so roughly 520px
wide keeps it crisp on high-resolution screens). If the file is ever missing the
sidebar falls back to the original wordmark rather than showing a broken image.
Date format
Dates are shown and entered as DD-MM-YYYY throughout: folder dates, the
Version Control and Legislation pages, the access-request table, the audit and
email logs, and the CSV exports. Internally they are stored in ISO form
(YYYY-MM-DD) so they sort and compare correctly, and converted for display.
Date fields accept DD-MM-YYYY and validate it as a real calendar date.
Folder dates
Every folder carries an Incident Date, chosen when the folder is created. From it a second date is derived:
- Incident Date — the date the material was recorded (
YYYY-MM-DD); labelled Incident Date on both View Data and Data Management (the underlying field is unchanged). - Legal Validity — always the incident date plus 30 days.
Both are shown under each folder on View Data, for locked folders as well as open ones. Legal Validity is never stored: it is calculated from the incident date on every request, so the two can never drift apart. A validity date in the past is marked expired.
The date is required when creating a folder, is validated as a real calendar date, and can be corrected later with the calendar button on a folder in Data Management. It cannot be cleared once set.
Folders created before this release have no date. They keep working and are listed with a no incident date marker plus a banner in Data Management, so an administrator can fill them in; until then their Legal Validity shows as not set.
Browsing folders in Data Management
The folder tree collapses. Each folder has an arrow to show or hide what is inside it, plus a count of its sub-folders and files and its two dates at a glance. Expand all and Collapse all act on the whole tree. The open/closed state is kept as you work, so uploading or renaming does not reset your place.
Legislation page
Every signed-in user gets a Legislation entry in the left menu listing the
laws, regulations, and internal rules governing the data. The content is
admin-managed from Data Management → Legislation, where entries can be
added, edited, and deleted. Each entry has an optional reference (e.g.
GDPR Art. 6), a required title, an in-force date, a summary, an optional link
to the full text, and a display-order number. All three actions are audited.
Access validity and the countdown
When an administrator approves a request they choose how long the access lasts:
| Option | Effect |
|---|---|
| Valid for 24 hours | expires 24 hours after approval |
| Valid for 15 days | expires 15 days after approval |
| Valid for 30 days | expires 30 days after approval |
| Valid forever | never expires |
Every window is measured from the moment the approval is confirmed, not from the request date.
On View Data each folder shows an Access field:
- a live countdown (
14d 03h 21m 07s) for timed access, turning amber in the final hour; - Access granted — no expiry for permanent access;
- Full access (administrator) for administrators;
- Access Denied when the user has no access, or once the window has lapsed.
The countdown runs against the server's clock (the server sends its time with the folder list), so a wrong clock on someone's machine cannot make access look longer than it is. When a window lapses in front of the user the page refreshes itself, the folder's contents disappear again, and the user can submit a fresh request.
Expiry is enforced on the server, not just in the interface: an expired approval stops the folder listing and blocks the download route. Access granted on a folder still cascades to everything inside it for as long as it is valid.
Access request notifications
The moment a user requests access, every administrator is emailed that a request is waiting for approval. The message states who asked and for what, the request reference, how many requests are currently pending, and carries a Review pending requests button linking straight to Data Management.
Set the portal address under Data Management → Email Server → Portal address so that button points at your real hostname. Without it the link falls back to the address the request came in on.
A few deliberate behaviours:
- If no mail server is configured the notification is recorded as queued in the Email Log, and Data Management shows a warning banner saying administrators are not receiving these emails.
- If delivery fails the failure is recorded in the Email Log and the audit log, but the access request itself is still created — mail problems never lose a request.
- The wording lives in the
request_to_admintemplate and is fully editable. Upgrades never overwrite a template you have customised; use Restore default in the template editor if you want to adopt the version shipped with a new release.
Storage page (administrators)
Storage in the Administration section of the menu shows where space is going.
Server disk — total, used, and free space on the filesystem holding the
uploads directory, with a bar that turns amber past 75% and red past 90%, and a
warning when the disk is nearly full. Free space is reported the way df does
(excluding blocks reserved for root), so the numbers match what you see on the
host.
Used by Infosec — how much of that space this application accounts for, split into uploaded files, brand images, and the database (including its write-ahead log), each with the path it lives at.
Space used per folder — every folder with two figures: total (the folder and everything nested inside it) and own (only the files sitting directly in it), plus a file count and a relative bar. Sort by size or by name. Files stored outside any folder are listed separately.
Consistency — cross-checks the database against the disk and reports:
- files recorded in the database but missing from disk (they appear in listings but cannot be downloaded);
- files on disk that no record points at, with a Reclaim space button to
delete them. That action is audited as
STORAGE_CLEANUP.
In Docker the figures describe the filesystem backing the mounted volume, which is what determines whether uploads will succeed.
Mail server & email log
Configuring the mail server
Data Management → Email Server configures SMTP from inside the application — host, port, username, password, from-address, implicit TLS, and certificate verification. Two buttons help you check it before relying on it:
- Test connection opens a connection and authenticates without saving.
- Send test message sends a real message using the saved settings.
Settings saved here take precedence over the SMTP_* environment variables. If
you have an existing .env-based setup, it keeps working untouched — the page
simply shows the values as coming from the environment until you override them.
The stored password is never sent back to the browser; the field shows a mask,
and saving the form with the mask left alone keeps the existing password. Note
that the password is stored in the database in plain text, so treat
data/datahub.db as a secret and keep file permissions tight.
Email log
Data Management → Email Log records every message the system sends: access
requests, approvals and denials, log exports, and test messages. Each entry
records time, direction, status (delivered, queued, or failed), sender,
recipient, subject, a body preview, attachments, the triggering context, and
the failure reason when delivery fails. Messages produced while no mail server
is configured are recorded as queued rather than lost.
The log can be filtered by text, direction, and status, then exported as CSV or emailed (the filtered set is attached as a CSV) to a specific address or to all administrators.
On received mail: Infosec sends mail but has no mailbox of its own, so there
is nothing to poll for incoming messages. The log stores direction as a first
class field and accepts inbound entries via POST /api/mail/log/inbound, so a
forwarder or mail-hook can record replies alongside outgoing mail. Automatic
collection would need IMAP credentials and a polling job, which is not part of
this release.
Managing users
The wizard creates only the first administrator. Everyone else is added from Data Management → Users:
- New user — username, email (required), role, and an initial password (minimum 10 characters with a letter and a number).
- Edit — change email or role, or set a new password.
- Reset MFA — turns two-factor authentication off for someone who has lost their authenticator app, so they can set it up again from My Account.
- Delete — removes the account. Data they published and their entries in the audit log are kept; only their pending access requests are removed.
Two safeguards are enforced by the server: you cannot delete the account you are signed in with, and the last remaining administrator can be neither deleted nor demoted.
Email is mandatory on every account. It is how approval notifications reach people, so an account without one cannot do its job. The rule is applied in three places: the browser form, the API (creating or updating an account), and — for databases created by this release onwards — a database constraint. An existing account can have its address changed but never cleared.
If a database created by an earlier release contains an account with no address, the upgrade does not lock that user out. Instead the account is listed with a missing — required badge, a banner appears above the user list, and a warning is written to the server log at startup, so an administrator can add the address.
Every one of these actions is written to the audit log (USER_CREATED,
USER_UPDATED, USER_DELETED, USER_MFA_RESET).
Email (SMTP)
Email is optional for evaluation. If you don't configure SMTP, every outgoing
message is written to the Logs as a MAIL_OUTBOX entry so nothing is lost
and you can see exactly what would have been sent.
To send real email, set these in .env:
SMTP_HOST=smtp.yourprovider.com
SMTP_PORT=587
SMTP_SECURE=false
SMTP_USER=your-username
SMTP_PASS=your-password
MAIL_FROM=Infosec <no-reply@yourdomain.com>
Admin notification recipients are simply every user whose role is admin.
How the approval flow works
- A user browses View Data and clicks Request access on a file or folder.
- An email (using the configurable "Request received" template) goes to all admins.
- An admin opens Data Management → Access Requests and clicks Approve or Deny.
- The requester receives an email (the "approved" or "declined" template).
- Approved users can now Download the item from View Data. Admins can always download.
Configurable emails & images
In Data Management:
- Email Templates — edit the subject and HTML body of each notification.
Insert placeholders like
{{username}},{{target_name}},{{email}},{{target_type}},{{created_at}}(click a chip to insert). Use Preview to see the rendered result with sample data. The footer is added automatically. - Brand Images — upload logos/images. Each uploaded image shows up as an
insertable chip in the template editor, dropping a ready-to-use
<img>tag into the body.
Logs
Every meaningful action is recorded: logins, MFA changes, folder/file create·rename·move·delete, uploads, access requests, approvals/denials, downloads, template edits, image uploads, and email dispatch.
From the Logs page you can filter by text/action/page, Export CSV, or Email logs (sends the filtered set as a CSV attachment to an address or to all admins).
Project layout
datahub/
├── UPGRADE.md # in-place upgrade guide for existing installs
├── Dockerfile # container image (multi-stage, non-root, healthcheck)
├── docker-compose.yml # one-command run with persistent volumes
├── docker-entrypoint.sh # ensures dirs, runs DB init, starts the server
├── .dockerignore
├── server.js # app entry, session + route wiring, page gating
├── db.js # SQLite schema & connection
├── init-db.js # create first admin / seed demo users
├── lib/
│ ├── audit.js # log() helper
│ ├── mailer.js # nodemailer transport, mail logging, no-SMTP fallback
│ ├── settings.js # key/value settings helper
│ ├── dates.js # recorded date + derived legal validity (+30 days)
│ ├── validate.js # shared account field rules (email is mandatory)
│ └── templates.js # email template store, render, defaults
├── middleware/auth.js # requireAuth / requireAdmin
├── routes/
│ ├── setup.js # first-boot wizard API (creates the first admin)
│ ├── users.js # admin user management (create/edit/reset/delete)
│ ├── versions.js # Version Control entries (admin only)
│ ├── legislation.js # Legislation entries (read: all, write: admin)
│ ├── mail.js # SMTP configuration + mail log (export / email)
│ ├── storage.js # disk usage + per-folder space (admin)
│ ├── auth.js # login, logout, session, MFA
│ ├── manage.js # admin folder/file CRUD + upload (Data Management)
│ ├── datacreate.js # Data Creation: build Document/Timeline HTML files (admin)
│ ├── data.js # View Data tree, requests, approvals, downloads
│ ├── settings.js # email templates + brand images
│ └── logs.js # list, CSV export, email export
├── public/ # setup, login, view-data, version-control, legislation,
│ # data-management, storage, account, logs + css/js
│ └── assets/ # infosec-logo.png (sidebar logo on signed-in pages)
├── avatars/ # per-user profile pictures (Docker volume)
├── data/ # SQLite database (created at runtime)
├── uploads/ # stored files (created at runtime)
└── brand/ # uploaded brand images (created at runtime)
Security notes for production
- Serve behind HTTPS and set
COOKIE_SECURE=truein.env. - Set a strong, unique
SESSION_SECRET. - Passwords are hashed with bcrypt; MFA uses TOTP (RFC 6238).
- Consider putting the app behind a reverse proxy (nginx/Caddy) and adding
rate-limiting and backups of the
data/,uploads/, andbrand/folders. - Uploaded files are served only through the authenticated, approval-checked download route — never from a public static path.
License
MIT.