From 5482731e7883e77fbef1ed0ca4993bcfd6ca0df7 Mon Sep 17 00:00:00 2001 From: jpmvaz Date: Sun, 13 Sep 2026 20:07:03 +0100 Subject: [PATCH] v_2.2 --- .dockerignore | 7 + Dockerfile | 41 + OLD_VERSIONS/v1.0/pst-indexer.zip | Bin 0 -> 27473 bytes OLD_VERSIONS/v1.2/pst-indexer.zip | Bin 0 -> 51421 bytes OLD_VERSIONS/v1.3/pst-indexer.zip | Bin 0 -> 55233 bytes OLD_VERSIONS/v1.4/v1.4.zip | Bin 0 -> 63171 bytes OLD_VERSIONS/v2.0/pst-indexer.zip | Bin 0 -> 71234 bytes OLD_VERSIONS/v2.0/pst-indexer/.dockerignore | 7 + OLD_VERSIONS/v2.0/pst-indexer/Dockerfile | 41 + OLD_VERSIONS/v2.0/pst-indexer/README.md | 1098 +++++++++++++++ .../v2.0/pst-indexer/admin_frontend/admin.css | 940 +++++++++++++ .../v2.0/pst-indexer/admin_frontend/admin.js | 866 ++++++++++++ .../pst-indexer/admin_frontend/index.html | 318 +++++ .../v2.0/pst-indexer/backend/admin_app.py | 587 ++++++++ OLD_VERSIONS/v2.0/pst-indexer/backend/app.py | 835 +++++++++++ OLD_VERSIONS/v2.0/pst-indexer/backend/auth.py | 513 +++++++ .../v2.0/pst-indexer/backend/requirements.txt | 10 + .../v2.0/pst-indexer/docker-compose.yml | 20 + OLD_VERSIONS/v2.0/pst-indexer/entrypoint.sh | 31 + OLD_VERSIONS/v2.0/pst-indexer/frontend/app.js | 869 ++++++++++++ .../v2.0/pst-indexer/frontend/index.html | 256 ++++ .../v2.0/pst-indexer/frontend/styles.css | 1136 +++++++++++++++ OLD_VERSIONS/v2.1 | 1 + OLD_VERSIONS/v2.2/pst-indexer.zip | Bin 0 -> 74484 bytes README.md | 1172 ++++++++++++++++ admin_frontend/admin.css | 980 +++++++++++++ admin_frontend/admin.js | 867 ++++++++++++ admin_frontend/index.html | 321 +++++ backend/admin_app.py | 597 ++++++++ backend/app.py | 1234 +++++++++++++++++ backend/auth.py | 520 +++++++ backend/requirements.txt | 10 + docker-compose.yml | 19 + entrypoint.sh | 25 + frontend/app.js | 877 ++++++++++++ frontend/index.html | 259 ++++ frontend/styles.css | 1182 ++++++++++++++++ 37 files changed, 15639 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 OLD_VERSIONS/v1.0/pst-indexer.zip create mode 100644 OLD_VERSIONS/v1.2/pst-indexer.zip create mode 100644 OLD_VERSIONS/v1.3/pst-indexer.zip create mode 100644 OLD_VERSIONS/v1.4/v1.4.zip create mode 100644 OLD_VERSIONS/v2.0/pst-indexer.zip create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/.dockerignore create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/Dockerfile create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/README.md create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/admin_frontend/admin.css create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/admin_frontend/admin.js create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/admin_frontend/index.html create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/backend/admin_app.py create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/backend/app.py create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/backend/auth.py create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/backend/requirements.txt create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/docker-compose.yml create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/entrypoint.sh create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/frontend/app.js create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/frontend/index.html create mode 100644 OLD_VERSIONS/v2.0/pst-indexer/frontend/styles.css create mode 160000 OLD_VERSIONS/v2.1 create mode 100644 OLD_VERSIONS/v2.2/pst-indexer.zip create mode 100644 README.md create mode 100644 admin_frontend/admin.css create mode 100644 admin_frontend/admin.js create mode 100644 admin_frontend/index.html create mode 100644 backend/admin_app.py create mode 100644 backend/app.py create mode 100644 backend/auth.py create mode 100644 backend/requirements.txt create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh create mode 100644 frontend/app.js create mode 100644 frontend/index.html create mode 100644 frontend/styles.css diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d52803e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +data/ +**/__pycache__ +**/*.pyc +.git +.gitignore +*.md +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..425b319 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,41 @@ +FROM python:3.11-slim + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Build tools for libpff-python, curl for healthcheck, libjpeg for qrcode PIL +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + libjpeg-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY backend/requirements.txt /app/backend/requirements.txt +RUN pip install --upgrade pip && \ + pip install -r /app/backend/requirements.txt + +COPY backend /app/backend +COPY frontend /app/frontend +COPY admin_frontend /app/admin_frontend +COPY entrypoint.sh /app/entrypoint.sh + +RUN chmod +x /app/entrypoint.sh && \ + mkdir -p /app/data + +ENV DATA_DIR=/app/data \ + FRONTEND_DIR=/app/frontend \ + ADMIN_FRONTEND_DIR=/app/admin_frontend \ + PYTHONPATH=/app/backend \ + COOKIE_SECURE=false + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD curl -fsS http://localhost:8000/api/health || exit 1 + +CMD ["/app/entrypoint.sh"] diff --git a/OLD_VERSIONS/v1.0/pst-indexer.zip b/OLD_VERSIONS/v1.0/pst-indexer.zip new file mode 100644 index 0000000000000000000000000000000000000000..957841b21a49bef4fb18b8af8660003cab563f68 GIT binary patch literal 27473 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 literal 0 HcmV?d00001 diff --git a/OLD_VERSIONS/v1.2/pst-indexer.zip b/OLD_VERSIONS/v1.2/pst-indexer.zip new file mode 100644 index 0000000000000000000000000000000000000000..d25e6578c42594a6bc77c42d98e7008b3665e093 GIT binary patch literal 51421 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

jrZ4Li= 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 z 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 literal 0 HcmV?d00001 diff --git a/OLD_VERSIONS/v1.4/v1.4.zip b/OLD_VERSIONS/v1.4/v1.4.zip new file mode 100644 index 0000000000000000000000000000000000000000..335f9e5575a73fc1663346332f7d693536db8115 GIT binary patch literal 63171 zcmaI7V~{35(w(sn>-+TA&M(nMKtg490s>*+r z=j16xSx_)Ep#RZgBGYyLYx3V6_`lJOnSqT_@xNW5fW*zxbu!@1NcjFO@&7ZF|F|%i z*c)4$I$4_A**lrykN$xNB8CI&+brQ?lGA}it8WBy1mhEtKLt=?TNY;V7`gRn2oL53 zXs7*7(z#S}x?n|?jU~$qZ`*+X&s{)3=>JDJ|0Br%3dhjI*3wSj%*o!)#njH^{}Y{k zVY<#Xu2rr*JRq9@rj#UeL^-Y(q|qK6KZ}GMWYBj{>~qOjMEThN zDLkDdW|;O_7v*x{!fPcVqV{@ZdLQmb!}Zx!*Z8FAn)^7h-=0jSD)hZ81NnRpF?PPE z^$N3vBOPIuK0ehA8vBc_JH|XnOcn+icks(bmhW}|5(_&ne0i2mGKS5G8csWoy@`$+ z&m@J5Nq{){%!7C&d6JT;xgPVlggzVwDb7@%&WWtvHaP))x;`|#ok!!B{v<_mA}ydg zfJOovlC`DAvWLXWpnUJlg$)_hIwyq5g*&$$TxE1{f@-KIaegQoxZmVYpLyDvWZv;D*(hfOUO;`{b8xblBWVnDz^?NaRdKyF&IguIjt^j$ zu36e^UQ$1SO4F-7UOCC*T9q5OFC6*TSJtco76^4mD6mF?=cf|PYk~lcpkBA9=K_&; zscY70c+uW08GIq(`ryP~IgJ;KTfMDiVcXR3E6PP-UM6OrhxlU=^J!#B^mdk(sQ5VE zwF?Vwwp^k6CM+hAbzvLhoso|c@|Q~xC1x5Le>hLmXi-gJn~)g>=H`)xl>a=%_a4Sw z@Z-Y|1SU|x*rD%Z4947uRJUN419$jLBq&~Z0WDa(I@|3B(1Ih~y@nI6PC{`yw$o=e=9uT4e?Vz8`5mW!FZQaDyyz zB61PxOF$4zX$3dfFmL1l9oODa-PnGA5r_y*z;q2)Ay+ZMGk*?Bc(R_ds#VWLGQ7~P zzu`^D>SFW&_aHZ{$TJy`nR>aePQ@XPUO>9ZH$;DL$WQ>?rA>e;1(#u=6W?rR)7=p9 zgg_xQ!kz;eQ^>EUi@!0PzSB^0P$j9_SC4aO z7+rf&JPt6)OdwmGu^*r}E)!PC^xJrNG^-KLx-&h6dUIns9~7QnKSJK1w~Tt1g6vMv zrjh<`1C#wZh&g5P6Nnsv{;5jt%Rfjx#IG2oD`m7Y2XAk0Ufl%MlA!fcCux>hHs91* z-Uzz7c>{fxwc+~{l7t-qL^0BtKLYS;>P~s&uw45F>qx#)V^0;?2H8eG8-HcTsEIO? zW>D|FAy@&m3RC?@)F8R<2&H^UX!(G`6t6|SEpLGjhMpV%>uerNP0uh<2t3p1%&If$ zkzg=!Qr?v2(?s(pR~AunKM$sb|0D52PY{n=eYLc3{wKq+ULK2lk9q6>L2%#e{hYLY z=NNs9D5jPbWk`rfxEkO9DMKv>xq$Z+pV6QIEQiXDJPvu~)8mJ{B&AzvrSE7r# z4_g;I1}*H{PItuy2m+M=hkzUy4=cO=VKB^pLg?~3*)Z5Kuz#G?5}BSWfBX&Dk=gB7 zYK_YgOGc!9-U`CQ_)yds!YG`4N{+oco|7upXGSTua>$%74BHshFSZn~y8e~+Qp{Qd z0da^i8I(##TcHIB`mR5VKM95ywq7F$MTTQ>7mpTO*$eiTJVgSy-V`ERw(F4VlKyNg z{K5qKW->)<&4*p(OL>nfLy#26*da3^A5Z5b&D-ccQh*D2H36I|M*+gQnyGs;c%Qs! zYaD}hnyP8ddTg~^eQ*fxmw7-`5B?

;5Y-UMCt`@2@tC< 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

u4WK4*ZQ~>D`w?f8 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 y@+VhBJ-iXCtRn_N(p%@tZCKQOt8w_Kjug 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 literal 0 HcmV?d00001 diff --git a/OLD_VERSIONS/v2.0/pst-indexer.zip b/OLD_VERSIONS/v2.0/pst-indexer.zip new file mode 100644 index 0000000000000000000000000000000000000000..b02810f322c59e9ec005386aecad4e1ecb6d47f7 GIT binary patch literal 71234 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 Ms6e0vy6vj|4?@b8H~;_u literal 0 HcmV?d00001 diff --git a/OLD_VERSIONS/v2.0/pst-indexer/.dockerignore b/OLD_VERSIONS/v2.0/pst-indexer/.dockerignore new file mode 100644 index 0000000..d52803e --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/.dockerignore @@ -0,0 +1,7 @@ +data/ +**/__pycache__ +**/*.pyc +.git +.gitignore +*.md +.DS_Store diff --git a/OLD_VERSIONS/v2.0/pst-indexer/Dockerfile b/OLD_VERSIONS/v2.0/pst-indexer/Dockerfile new file mode 100644 index 0000000..234c804 --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/Dockerfile @@ -0,0 +1,41 @@ +FROM python:3.11-slim + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 \ + PIP_DISABLE_PIP_VERSION_CHECK=1 + +# Build tools for libpff-python, curl for healthcheck, libjpeg for qrcode PIL +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + libjpeg-dev \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +COPY backend/requirements.txt /app/backend/requirements.txt +RUN pip install --upgrade pip && \ + pip install -r /app/backend/requirements.txt + +COPY backend /app/backend +COPY frontend /app/frontend +COPY admin_frontend /app/admin_frontend +COPY entrypoint.sh /app/entrypoint.sh + +RUN chmod +x /app/entrypoint.sh && \ + mkdir -p /app/data + +ENV DATA_DIR=/app/data \ + FRONTEND_DIR=/app/frontend \ + ADMIN_FRONTEND_DIR=/app/admin_frontend \ + PYTHONPATH=/app/backend \ + COOKIE_SECURE=false + +EXPOSE 8000 8001 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD curl -fsS http://localhost:8000/api/health && curl -fsS http://localhost:8001/api/health || exit 1 + +CMD ["/app/entrypoint.sh"] diff --git a/OLD_VERSIONS/v2.0/pst-indexer/README.md b/OLD_VERSIONS/v2.0/pst-indexer/README.md new file mode 100644 index 0000000..657990c --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/README.md @@ -0,0 +1,1098 @@ +# PST Archive — Mail Indexer + +A self-hosted, Dockerized application for **uploading Microsoft Outlook PST/OST files**, **indexing every email inside them**, and **searching the contents through a web browser** — with full **user authentication**, **multi-factor authentication (MFA)**, **encrypted-at-rest credentials**, and a **separate admin panel** on its own port. + +Designed for **forensic review, e-discovery, personal archive browsing, and migration audits** — any situation where you need to crack open a PST and actually find things inside it without loading it into Outlook. + +--- + +## Table of Contents + +1. [Features](#features) +2. [Quick Start](#quick-start) +3. [First-Run Setup](#first-run-setup) +4. [Running as a Daemon](#running-as-a-daemon) +5. [Theming & Dark Mode](#theming--dark-mode) +6. [Authentication & Security Model](#authentication--security-model) +7. [Admin Panel](#admin-panel) +8. [Organizing PSTs with Folders](#organizing-psts-with-folders) +9. [Folder Access Permissions](#folder-access-permissions) +10. [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa) +11. [Encryption at Rest](#encryption-at-rest) +12. [How It Works](#how-it-works) +13. [Architecture](#architecture) +14. [Project Layout](#project-layout) +15. [Data Model](#data-model) +16. [REST API Reference](#rest-api-reference) +17. [Frontend Interfaces](#frontend-interfaces) +18. [Search Syntax](#search-syntax) +19. [Configuration](#configuration) +20. [Persistence & Backup](#persistence--backup) +21. [Troubleshooting](#troubleshooting) +22. [Performance Notes](#performance-notes) +23. [Security Considerations](#security-considerations) +24. [Extending the App](#extending-the-app) + +--- + +## Features + +### Core +- **Drag-and-drop upload** of `.pst` and `.ost` files through the browser +- **Streaming upload with progress bar** — handles multi-gigabyte archives +- **Background indexing** into SQLite with **FTS5 full-text search** (ranked, prefix-matched, with highlighted snippets) +- **Multi-archive support**: switch between uploaded PSTs; each is searched independently +- **Folder organization**: create folders in the sidebar to group related PST files (e.g. by client, case, year). Move archives between folders, rename folders, or delete folders (any archives inside are safely moved to Uncategorized) +- **Per-folder access permissions**: admins can restrict which non-admin users can view each folder. A single user can be granted access to many folders; a single folder can be shared with many users (true many-to-many) +- **Folder filter**: narrow search to any folder within the archive (Inbox, Sent Items, nested folders) +- **Full message viewer**: subject, sender, recipients, date, folder, attachments, body +- **Persistent storage**: everything lives in a mounted `./data` volume + +### Security +- **First-run setup wizard** forces creation of an initial administrator before any other action is possible +- **Password authentication** using **Argon2id** (winner of the Password Hashing Competition) with constant-time verification +- **TOTP-based MFA** (RFC 6238; compatible with Google Authenticator, Authy, 1Password, Microsoft Authenticator, and every other standard authenticator app) +- **Encryption at rest** for all user PII — usernames, password hashes, and TOTP secrets are stored as Fernet (AES-128-CBC + HMAC-SHA256) ciphertexts +- **Master key** auto-generated on first run and stored with `chmod 600`, or overridable via `MASTER_SECRET` environment variable +- **HMAC-based username lookup** — usernames are never stored in plaintext but are still indexable for login +- **JWT session cookies**, signed with an HKDF-derived sub-key, HttpOnly, SameSite=Lax +- **Rate limiting** on login attempts (10 failures per 5 minutes per IP+username) +- **Admin-only user management**: create, delete, role-change, password-reset, MFA-enroll other users +- **Admin file & folder management**: system-wide view of every uploaded PST and every folder, with create/rename/delete for folders and force-delete for any file regardless of uploader +- **Separate admin panel on a separate port** — lockable behind its own firewall rule or VPN + +### UI +- **Light and dark themes** on both the main app and the admin panel, with a sun/moon toggle in the masthead and on login/setup screens +- Preference persists in `localStorage` per-browser and respects the operating system's `prefers-color-scheme` on first visit +- Theme applied inline in `` before stylesheets load — **no flash of wrong theme** on page load +- Dark mode uses a custom "inked paper" palette (deep charcoal with warm paper-cream text and softened oxblood accent) — intentional character, not generic-dark +- Editorial typography (Fraunces serif + JetBrains Mono), grain overlay, responsive layout down to mobile widths + +--- + +## Quick Start + +### Prerequisites + +- Docker 20.10 or newer +- Docker Compose v2 +- A `.pst` or `.ost` file to index + +### Run (as a daemon — recommended) + +```bash +docker compose up -d --build +``` + +The `-d` flag runs the container in the background (detached). On the first run this takes **3–5 minutes** because `libpff-python` compiles a C extension from source; subsequent startups are nearly instant. + +The container is configured with `restart: unless-stopped`, so it will automatically come back up after a reboot or a crash, and keep running across Docker daemon restarts — until you explicitly stop it. + +Once the container is up: + +- **Main app**: `http://localhost:8000` +- **Admin panel**: `http://localhost:8001` + +### Foreground mode (useful for debugging) + +```bash +docker compose up --build +``` + +This runs the container attached to your terminal so you can see logs inline. Press `Ctrl-C` to stop. + +### Tail the logs + +```bash +docker compose logs -f # follow both streams +docker compose logs -f pst-indexer # explicit service name +``` + +### Check status + +```bash +docker compose ps # is it running? +docker compose top # which processes? +``` + +### Stop + +```bash +docker compose stop # stop, keep data, keep container +docker compose down # stop and remove the container, keep data volume +``` + +### Full reset + +```bash +docker compose down +rm -rf ./data # erases everything including users and the master key +docker compose up -d --build +``` + +### Restart / update + +```bash +# After editing code or pulling new images: +docker compose up -d --build # rebuild & restart with one command +# Or just restart without rebuilding: +docker compose restart +``` + +--- + +## First-Run Setup + +On the very first visit to `http://localhost:8000`, you will be presented with a **setup wizard** that asks you to create the initial administrator: + +1. Choose a username (3–64 characters) +2. Choose a password (8+ characters — use a passphrase you'll remember) +3. Confirm the password + +The setup endpoint is only callable **while the users table is empty**. Once the first admin is created, the endpoint returns `409 Conflict` and is effectively disabled. + +After setup, you are automatically signed in and landed on the main app. + +**Heads up:** the admin panel on port 8001 will refuse to accept any logins until the first admin has been created via the main app's setup wizard. This prevents a race where someone could hit `/api/login` on the admin port before you've set things up. + +--- + +## Running as a Daemon + +By default, this app is designed to **run as a long-lived background service**. The `docker-compose.yml` sets `restart: unless-stopped`, which means once started, the container will: + +- Keep running after you close your terminal +- Automatically restart if the process crashes +- Automatically restart when the host reboots and Docker comes back up +- **Not** restart if you explicitly stop it with `docker compose stop` (hence "unless-stopped") + +### Starting the daemon + +```bash +docker compose up -d --build +``` + +The `-d` flag is short for "detached" — the container is launched in the background and the command returns immediately. Your terminal is free to do other things; the container keeps running. + +### Verifying it's running + +```bash +docker compose ps +``` + +You should see output like: + +``` +NAME IMAGE STATUS PORTS +pst-indexer pst-indexer-pst-indexer Up 2 minutes (healthy) 0.0.0.0:8000->8000/tcp, 0.0.0.0:8001->8001/tcp +``` + +The `(healthy)` flag comes from the built-in healthcheck, which probes both `/api/health` endpoints every 30 seconds. + +### Watching logs + +Since the container is detached, you won't see logs in your terminal by default. Attach to them on demand: + +```bash +docker compose logs # print recent logs and exit +docker compose logs -f # follow (live tail), Ctrl-C to detach +docker compose logs -f --tail=100 # follow, starting from the last 100 lines +docker compose logs --since 1h # only logs from the last hour +``` + +Detaching from `-f` does **not** stop the container — it just stops showing you the logs. + +### Stopping + +```bash +docker compose stop # stop the container but keep it around +docker compose start # start it again if previously stopped +docker compose restart # stop and start in one command +docker compose down # stop and remove the container (data volume is kept) +docker compose down -v # stop, remove container, AND remove anonymous volumes +``` + +None of these delete your `./data` directory — that's a bind mount, not a Docker-managed volume, so it's always yours. + +### Survives reboots + +Because `restart: unless-stopped` is set, after a host reboot Docker will automatically start the container as soon as the Docker daemon comes up. Nothing manual required on your part. + +If you ever explicitly stopped it with `docker compose stop`, Docker will *not* auto-start it after reboot (that's the whole point of "unless-stopped"). Run `docker compose start` or `docker compose up -d` to bring it back. + +### Updating to a new version + +```bash +# Pull new source, then: +docker compose up -d --build +``` + +Docker Compose compares the current image to the built one; if they differ, it rebuilds and recreates the container with zero-downtime-style replacement. Your `./data` volume is preserved throughout. + +--- + +## Theming & Dark Mode + +Both the main app and the admin panel support **light and dark themes**, with a sun/moon toggle visible in the masthead (when signed in) and floating in the top-right corner on the login/setup screens. + +### How the toggle works + +- Click the icon to flip between themes +- Choice is saved to `localStorage["pst-theme"]` in your browser +- On a fresh browser (nothing saved), the app respects your operating system's `prefers-color-scheme` setting +- The theme is applied by an inline ` + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OLD_VERSIONS/v2.0/pst-indexer/backend/admin_app.py b/OLD_VERSIONS/v2.0/pst-indexer/backend/admin_app.py new file mode 100644 index 0000000..e79bf8f --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/backend/admin_app.py @@ -0,0 +1,587 @@ +""" +PST Indexer — Admin Panel (port 8001). + +Separate FastAPI instance on its own port so operators can place it behind a +more restrictive firewall rule or VPN than the main app. + +Exposes: +- Admin login (separate session cookie, scoped "admin") +- List, create, delete users +- Toggle admin role +- Reset passwords +- Enroll / confirm / disable MFA for any user (admin-managed flow) +- Self-service MFA for the currently logged-in admin +""" +import io +import os +from pathlib import Path +from typing import Optional + +import qrcode +from fastapi import ( + Depends, FastAPI, HTTPException, Request, Response, status, +) +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +import auth +from auth import ( + ADMIN_SESSION_COOKIE_NAME, SESSION_TTL_SECONDS, + User, begin_mfa_enrollment, confirm_mfa_enrollment, count_admins, + count_users, create_session_token, create_user, delete_user, + disable_mfa, find_user_by_id, find_user_by_username, + get_totp_secret, list_users, login_limiter, record_login, + require_admin_user, set_user_role, totp_provisioning_uri, + update_password, verify_password, verify_totp, +) + +ADMIN_FRONTEND_DIR = Path(os.environ.get("ADMIN_FRONTEND_DIR", "/app/admin_frontend")) +COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "false").lower() == "true" + +app = FastAPI(title="PST Indexer Admin Panel") +app.add_middleware( + CORSMiddleware, + allow_origins=[], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.on_event("startup") +def _startup(): + auth.init_auth_schema() + + +def _set_admin_cookie(response: Response, token: str): + response.set_cookie( + key=ADMIN_SESSION_COOKIE_NAME, + value=token, + max_age=SESSION_TTL_SECONDS, + httponly=True, + secure=COOKIE_SECURE, + samesite="lax", + path="/", + ) + + +def _clear_admin_cookie(response: Response): + response.delete_cookie(key=ADMIN_SESSION_COOKIE_NAME, path="/") + + +# --------------------------------------------------------------------------- +# Auth +# --------------------------------------------------------------------------- +class LoginPayload(BaseModel): + username: str + password: str + totp_code: Optional[str] = None + + +@app.get("/api/health") +def health(): + return {"status": "ok", "admins": count_admins(), "users": count_users()} + + +@app.get("/api/setup/status") +def setup_status(): + """True if no users exist. Admin UI redirects to main app to complete setup.""" + return {"needs_setup": count_users() == 0} + + +@app.post("/api/login") +def admin_login(payload: LoginPayload, request: Request, response: Response): + ip = request.client.host if request.client else "unknown" + rate_key = f"admin:{ip}:{payload.username.lower()}" + if not login_limiter.check(rate_key): + raise HTTPException(429, "Too many login attempts. Please wait a few minutes.") + + found = find_user_by_username(payload.username) + if not found: + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid credentials") + user, pw_hash = found + + if not verify_password(pw_hash, payload.password): + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid credentials") + + if not user.is_admin: + raise HTTPException(403, "Admin access required") + + if user.mfa_enabled: + if not payload.totp_code: + return JSONResponse( + status_code=401, + content={"detail": "MFA required", "mfa_required": True}, + ) + secret = get_totp_secret(user.id) + if not secret or not verify_totp(secret, payload.totp_code): + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid MFA code") + + login_limiter.reset(rate_key) + record_login(user.id) + token = create_session_token(user, scope="admin") + _set_admin_cookie(response, token) + return {"user": user.public_dict()} + + +@app.post("/api/logout") +def admin_logout(response: Response): + _clear_admin_cookie(response) + return {"ok": True} + + +@app.get("/api/me") +def me(user: User = Depends(require_admin_user)): + return {"user": user.public_dict()} + + +# --------------------------------------------------------------------------- +# User management +# --------------------------------------------------------------------------- +class CreateUserPayload(BaseModel): + username: str + password: str + role: str = "user" + + +class UpdatePasswordPayload(BaseModel): + password: str + + +class UpdateRolePayload(BaseModel): + role: str + + +@app.get("/api/users") +def api_list_users(_: User = Depends(require_admin_user)): + return [u.public_dict() for u in list_users()] + + +@app.post("/api/users") +def api_create_user( + payload: CreateUserPayload, + admin: User = Depends(require_admin_user), +): + try: + user = create_user(payload.username, payload.password, role=payload.role) + except ValueError as e: + raise HTTPException(400, str(e)) + return user.public_dict() + + +@app.delete("/api/users/{user_id}") +def api_delete_user(user_id: str, admin: User = Depends(require_admin_user)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + + # Prevent deleting yourself + if target.id == admin.id: + raise HTTPException(400, "You cannot delete your own account") + + # Prevent deleting the last admin + if target.is_admin and count_admins() <= 1: + raise HTTPException(400, "Cannot delete the last remaining admin") + + delete_user(user_id) + return {"deleted": user_id} + + +@app.put("/api/users/{user_id}/role") +def api_set_role( + user_id: str, + payload: UpdateRolePayload, + admin: User = Depends(require_admin_user), +): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + + if target.id == admin.id and payload.role != "admin": + raise HTTPException(400, "You cannot demote your own account") + + if (target.is_admin and payload.role != "admin" + and count_admins() <= 1): + raise HTTPException(400, "Cannot demote the last remaining admin") + + try: + set_user_role(user_id, payload.role) + except ValueError as e: + raise HTTPException(400, str(e)) + + return find_user_by_id(user_id).public_dict() + + +@app.put("/api/users/{user_id}/password") +def api_update_password( + user_id: str, + payload: UpdatePasswordPayload, + admin: User = Depends(require_admin_user), +): + if not find_user_by_id(user_id): + raise HTTPException(404, "User not found") + try: + update_password(user_id, payload.password) + except ValueError as e: + raise HTTPException(400, str(e)) + return {"ok": True} + + +# --------------------------------------------------------------------------- +# MFA management +# --------------------------------------------------------------------------- +class ConfirmMfaPayload(BaseModel): + totp_code: str + + +@app.post("/api/users/{user_id}/mfa/begin") +def api_begin_mfa(user_id: str, admin: User = Depends(require_admin_user)): + """Provision a new TOTP secret for the target user (admin action). + Returns the secret + otpauth URI so the admin can show it to the user.""" + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + secret, uri = begin_mfa_enrollment(user_id) + return { + "secret": secret, + "otpauth_uri": uri, + "username": target.username, + "qr_url": f"/api/users/{user_id}/mfa/qr", + } + + +@app.get("/api/users/{user_id}/mfa/qr") +def api_mfa_qr(user_id: str, admin: User = Depends(require_admin_user)): + """Render a PNG QR code of the target user's current (unconfirmed) TOTP URI.""" + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + secret = get_totp_secret(user_id) + if not secret: + raise HTTPException(400, "No MFA enrollment in progress") + uri = totp_provisioning_uri(secret, target.username) + + qr = qrcode.QRCode(box_size=6, border=2) + qr.add_data(uri) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + buf = io.BytesIO() + img.save(buf, format="PNG") + buf.seek(0) + return StreamingResponse(buf, media_type="image/png") + + +@app.post("/api/users/{user_id}/mfa/confirm") +def api_confirm_mfa( + user_id: str, + payload: ConfirmMfaPayload, + admin: User = Depends(require_admin_user), +): + if not find_user_by_id(user_id): + raise HTTPException(404, "User not found") + if not confirm_mfa_enrollment(user_id, payload.totp_code): + raise HTTPException(400, "Invalid MFA code — enrollment not confirmed") + return find_user_by_id(user_id).public_dict() + + +@app.post("/api/users/{user_id}/mfa/disable") +def api_disable_mfa(user_id: str, admin: User = Depends(require_admin_user)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + disable_mfa(user_id) + return find_user_by_id(user_id).public_dict() + + +# --------------------------------------------------------------------------- +# Folder management (admin) +# --------------------------------------------------------------------------- +import uuid as _uuid +from datetime import datetime, timezone +from pathlib import Path as _Path +from auth import get_db + + +UPLOAD_DIR = _Path(os.environ.get("DATA_DIR", "/app/data")) / "uploads" + + +class AdminFolderPayload(BaseModel): + name: str + + +def _validate_name(name: str) -> str: + name = (name or "").strip() + if not name: + raise HTTPException(400, "Folder name cannot be empty") + if len(name) > 80: + raise HTTPException(400, "Folder name too long (max 80 characters)") + return name + + +@app.get("/api/folders") +def admin_list_folders(_: User = Depends(require_admin_user)): + with get_db() as conn: + rows = conn.execute( + """SELECT f.id, f.name, f.created_at, f.created_by, + COUNT(p.id) AS file_count + FROM pst_folders f + LEFT JOIN pst_files p ON p.folder_id = f.id + GROUP BY f.id + ORDER BY f.name COLLATE NOCASE""" + ).fetchall() + return [dict(r) for r in rows] + + +@app.post("/api/folders") +def admin_create_folder(payload: AdminFolderPayload, + admin: User = Depends(require_admin_user)): + name = _validate_name(payload.name) + with get_db() as conn: + existing = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) + ).fetchone() + if existing: + raise HTTPException(409, "A folder with that name already exists") + folder_id = _uuid.uuid4().hex + conn.execute( + """INSERT INTO pst_folders (id, name, created_at, created_by) + VALUES (?, ?, ?, ?)""", + (folder_id, name, datetime.now(timezone.utc).isoformat(), admin.id), + ) + conn.commit() + return {"id": folder_id, "name": name, "file_count": 0} + + +@app.put("/api/folders/{folder_id}") +def admin_rename_folder(folder_id: str, payload: AdminFolderPayload, + admin: User = Depends(require_admin_user)): + name = _validate_name(payload.name) + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + clash = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", + (name, folder_id), + ).fetchone() + if clash: + raise HTTPException(409, "A folder with that name already exists") + conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", + (name, folder_id)) + conn.commit() + return {"id": folder_id, "name": name} + + +@app.delete("/api/folders/{folder_id}") +def admin_delete_folder(folder_id: str, + admin: User = Depends(require_admin_user)): + """Delete a folder. PSTs inside it become uncategorized (folder_id = NULL).""" + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", + (folder_id,)) + conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) + conn.commit() + return {"deleted": folder_id} + + +# --------------------------------------------------------------------------- +# PST file management (admin) +# --------------------------------------------------------------------------- +@app.get("/api/pst-files") +def admin_list_pst_files(_: User = Depends(require_admin_user)): + with get_db() as conn: + rows = conn.execute( + """SELECT p.id, p.original_name, p.uploaded_at, p.size_bytes, + p.status, p.message_count, p.error, + p.folder_id, f.name AS folder_name, + p.uploaded_by + FROM pst_files p + LEFT JOIN pst_folders f ON f.id = p.folder_id + ORDER BY p.uploaded_at DESC""" + ).fetchall() + return [dict(r) for r in rows] + + +@app.delete("/api/pst-files/{pst_id}") +def admin_delete_pst(pst_id: str, admin: User = Depends(require_admin_user)): + with get_db() as conn: + row = conn.execute( + "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) + ).fetchone() + if not row: + raise HTTPException(404, "PST not found") + conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) + conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) + conn.commit() + try: + (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) + except Exception: + pass + return {"deleted": pst_id} + + +class AdminAssignPayload(BaseModel): + folder_id: Optional[str] = None + + +@app.put("/api/pst-files/{pst_id}/folder") +def admin_assign_pst_to_folder(pst_id: str, payload: AdminAssignPayload, + admin: User = Depends(require_admin_user)): + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_files WHERE id = ?", + (pst_id,)).fetchone(): + raise HTTPException(404, "PST not found") + if payload.folder_id: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (payload.folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", + (payload.folder_id, pst_id)) + conn.commit() + return {"pst_id": pst_id, "folder_id": payload.folder_id} + + +# --------------------------------------------------------------------------- +# Folder permissions (admin-only) +# Controls which user can access which folder. +# Admins ALWAYS have access to every folder regardless of this table — +# these grants are for non-admin users. +# --------------------------------------------------------------------------- +class PermissionToggle(BaseModel): + granted: bool + + +@app.get("/api/permissions") +def admin_list_permissions(_: User = Depends(require_admin_user)): + """Return the full matrix: users, folders, and all grants.""" + with get_db() as conn: + users = conn.execute("SELECT * FROM users ORDER BY created_at ASC").fetchall() + folders = conn.execute( + "SELECT id, name FROM pst_folders ORDER BY name COLLATE NOCASE" + ).fetchall() + grants = conn.execute( + "SELECT user_id, folder_id, granted_at FROM folder_permissions" + ).fetchall() + + from auth import _row_to_user + user_list = [] + for u in users: + pub = _row_to_user(u).public_dict() + user_list.append(pub) + + return { + "users": user_list, + "folders": [dict(f) for f in folders], + "grants": [dict(g) for g in grants], + } + + +@app.get("/api/users/{user_id}/folders") +def admin_user_folders(user_id: str, _: User = Depends(require_admin_user)): + """Folders a given user has been granted.""" + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + with get_db() as conn: + rows = conn.execute( + """SELECT f.id, f.name, p.granted_at + FROM folder_permissions p + JOIN pst_folders f ON f.id = p.folder_id + WHERE p.user_id = ? + ORDER BY f.name COLLATE NOCASE""", + (user_id,), + ).fetchall() + return { + "user": target.public_dict(), + "folders": [dict(r) for r in rows], + } + + +@app.get("/api/folders/{folder_id}/users") +def admin_folder_users(folder_id: str, _: User = Depends(require_admin_user)): + """Users granted on a given folder.""" + with get_db() as conn: + folder = conn.execute( + "SELECT id, name FROM pst_folders WHERE id = ?", (folder_id,) + ).fetchone() + if not folder: + raise HTTPException(404, "Folder not found") + + grant_rows = conn.execute( + """SELECT user_id, granted_at + FROM folder_permissions WHERE folder_id = ?""", + (folder_id,), + ).fetchall() + + # Hydrate user details (usernames are encrypted — need decrypt via find_user_by_id) + users_out = [] + for r in grant_rows: + u = find_user_by_id(r["user_id"]) + if u: + users_out.append({ + **u.public_dict(), + "granted_at": r["granted_at"], + }) + users_out.sort(key=lambda x: x["username"].lower()) + return { + "folder": dict(folder), + "users": users_out, + } + + +@app.put("/api/permissions/{user_id}/{folder_id}") +def admin_toggle_permission( + user_id: str, + folder_id: str, + payload: PermissionToggle, + admin: User = Depends(require_admin_user), +): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + + if target.is_admin: + # Admins implicitly have access to everything; refuse explicit grants + # so the UI doesn't confuse admins with checked-but-meaningless rows. + raise HTTPException( + 400, + "Admins have access to all folders automatically; permissions " + "only apply to non-admin users.", + ) + + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + + if payload.granted: + conn.execute( + """INSERT OR IGNORE INTO folder_permissions + (user_id, folder_id, granted_at, granted_by) + VALUES (?, ?, ?, ?)""", + (user_id, folder_id, + datetime.now(timezone.utc).isoformat(), admin.id), + ) + else: + conn.execute( + "DELETE FROM folder_permissions WHERE user_id = ? AND folder_id = ?", + (user_id, folder_id), + ) + conn.commit() + + return {"user_id": user_id, "folder_id": folder_id, "granted": payload.granted} + + +# --------------------------------------------------------------------------- +# Static admin frontend +# --------------------------------------------------------------------------- +if ADMIN_FRONTEND_DIR.exists(): + app.mount("/", StaticFiles(directory=str(ADMIN_FRONTEND_DIR), html=True), name="admin") diff --git a/OLD_VERSIONS/v2.0/pst-indexer/backend/app.py b/OLD_VERSIONS/v2.0/pst-indexer/backend/app.py new file mode 100644 index 0000000..f462edf --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/backend/app.py @@ -0,0 +1,835 @@ +""" +PST Indexer — Main application (port 8000). + +Serves: +- The web UI (login, setup wizard, archive browser) +- REST API for PST upload, indexing, search +- All data-access endpoints require an authenticated session + +First-run behavior: +- If no users exist, the UI routes users to a setup wizard. +- /api/setup is only callable while the users table is empty; after the first + admin is created, the endpoint returns 409. +""" +import os +import re +import sqlite3 +import uuid +import asyncio +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +import aiofiles +import pypff +from fastapi import ( + BackgroundTasks, Depends, FastAPI, File, HTTPException, + Request, Response, UploadFile, status, +) +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, FileResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +import auth +from auth import ( + SESSION_COOKIE_NAME, SESSION_TTL_SECONDS, + User, count_users, count_admins, create_user, find_user_by_username, + find_user_by_id, get_totp_secret, verify_password, verify_totp, + record_login, create_session_token, login_limiter, require_app_user, + get_db, +) + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- +DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) +UPLOAD_DIR = DATA_DIR / "uploads" +FRONTEND_DIR = Path(os.environ.get("FRONTEND_DIR", "/app/frontend")) + +UPLOAD_DIR.mkdir(parents=True, exist_ok=True) +DATA_DIR.mkdir(parents=True, exist_ok=True) + +# Cookie security. Flip to True when serving over HTTPS. +COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "false").lower() == "true" + + +# --------------------------------------------------------------------------- +# App DB schema (messages + pst_files) +# --------------------------------------------------------------------------- +def init_app_schema(): + with get_db() as conn: + conn.executescript(""" + CREATE TABLE IF NOT EXISTS pst_folders ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at TEXT NOT NULL, + created_by TEXT + ); + + CREATE TABLE IF NOT EXISTS folder_permissions ( + user_id TEXT NOT NULL, + folder_id TEXT NOT NULL, + granted_at TEXT NOT NULL, + granted_by TEXT, + PRIMARY KEY (user_id, folder_id) + ); + + CREATE INDEX IF NOT EXISTS idx_folder_perms_user ON folder_permissions(user_id); + CREATE INDEX IF NOT EXISTS idx_folder_perms_folder ON folder_permissions(folder_id); + + CREATE TABLE IF NOT EXISTS pst_files ( + id TEXT PRIMARY KEY, + filename TEXT NOT NULL, + original_name TEXT NOT NULL, + uploaded_at TEXT NOT NULL, + size_bytes INTEGER NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + message_count INTEGER DEFAULT 0, + error TEXT, + uploaded_by TEXT, + folder_id TEXT + ); + + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pst_id TEXT NOT NULL, + folder TEXT, + subject TEXT, + sender TEXT, + recipients TEXT, + sent_date TEXT, + body TEXT, + attachments TEXT, + FOREIGN KEY (pst_id) REFERENCES pst_files(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_messages_pst ON messages(pst_id); + CREATE INDEX IF NOT EXISTS idx_messages_sent_date ON messages(sent_date); + CREATE INDEX IF NOT EXISTS idx_pst_files_folder ON pst_files(folder_id); + + CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( + subject, sender, recipients, body, folder, + content='messages', + content_rowid='id', + tokenize='porter unicode61' + ); + + CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN + INSERT INTO messages_fts(rowid, subject, sender, recipients, body, folder) + VALUES (new.id, new.subject, new.sender, new.recipients, new.body, new.folder); + END; + + CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN + INSERT INTO messages_fts(messages_fts, rowid, subject, sender, recipients, body, folder) + VALUES ('delete', old.id, old.subject, old.sender, old.recipients, old.body, old.folder); + END; + """) + # Backfill: add folder_id column to DBs created before this feature existed. + cols = [r["name"] for r in conn.execute("PRAGMA table_info(pst_files)").fetchall()] + if "folder_id" not in cols: + conn.execute("ALTER TABLE pst_files ADD COLUMN folder_id TEXT") + conn.execute("CREATE INDEX IF NOT EXISTS idx_pst_files_folder ON pst_files(folder_id)") + conn.commit() + + +# --------------------------------------------------------------------------- +# Access control helpers — which folders / PSTs is a given user allowed to see? +# +# Rules: +# - Admins see everything. +# - Regular users see only folders they have been explicitly granted, +# plus the implicit "Uncategorized" bucket (folder_id = NULL) — this is +# deliberately shared because otherwise newly-uploaded PSTs would be +# invisible until someone moved them into a permitted folder. +# - A PST is visible iff its folder is visible (or the PST is uncategorized). +# --------------------------------------------------------------------------- +def allowed_folder_ids_for(user) -> set[str]: + """Return the set of folder_ids a user is explicitly permitted on. + For admins this is not used — they see everything unconditionally.""" + with get_db() as conn: + rows = conn.execute( + "SELECT folder_id FROM folder_permissions WHERE user_id = ?", + (user.id,), + ).fetchall() + return {r["folder_id"] for r in rows} + + +def user_can_see_folder(user, folder_id: Optional[str]) -> bool: + """Can this user see content inside this folder? folder_id may be None + (Uncategorized), which is visible to everyone.""" + if user.is_admin: + return True + if folder_id is None: + return True # Uncategorized is shared + return folder_id in allowed_folder_ids_for(user) + + +def user_can_see_pst(user, pst_id: str) -> bool: + if user.is_admin: + return True + with get_db() as conn: + row = conn.execute( + "SELECT folder_id FROM pst_files WHERE id = ?", (pst_id,) + ).fetchone() + if not row: + return False + return user_can_see_folder(user, row["folder_id"]) + + +# --------------------------------------------------------------------------- +# PST indexing helpers (unchanged from pre-auth version) +# --------------------------------------------------------------------------- +def _safe_str(value) -> str: + if value is None: + return "" + if isinstance(value, bytes): + try: + return value.decode("utf-8", errors="replace") + except Exception: + return value.decode("latin-1", errors="replace") + return str(value) + + +def _extract_recipients(message) -> str: + parts = [] + try: + headers = _safe_str(getattr(message, "transport_headers", "")) + if headers: + for line in headers.split("\n"): + low = line.lower() + if low.startswith("to:") or low.startswith("cc:") or low.startswith("bcc:"): + parts.append(line.strip()) + except Exception: + pass + return " | ".join(parts) if parts else "" + + +def _extract_body(message) -> str: + for attr in ("plain_text_body", "html_body", "rtf_body"): + try: + body = getattr(message, attr, None) + if body: + text = _safe_str(body) + if attr == "html_body": + text = re.sub(r"<[^>]+>", " ", text) + text = re.sub(r"\s+", " ", text).strip() + if text: + return text + except Exception: + continue + return "" + + +def _extract_sender(message) -> str: + for attr in ("sender_name", "sender_email_address"): + try: + v = _safe_str(getattr(message, attr, "")) + if v: + return v + except Exception: + continue + try: + headers = _safe_str(getattr(message, "transport_headers", "")) + for line in headers.split("\n"): + if line.lower().startswith("from:"): + return line.split(":", 1)[1].strip() + except Exception: + pass + return "" + + +def _extract_date(message) -> Optional[str]: + try: + d = getattr(message, "delivery_time", None) or getattr(message, "client_submit_time", None) + if d: + if isinstance(d, datetime): + return d.isoformat() + return _safe_str(d) + except Exception: + pass + return None + + +def _extract_attachments(message) -> str: + names = [] + try: + count = getattr(message, "number_of_attachments", 0) + for i in range(count): + try: + att = message.get_attachment(i) + name = _safe_str(getattr(att, "name", "") or getattr(att, "long_filename", "")) + if name: + names.append(name) + except Exception: + continue + except Exception: + pass + return " | ".join(names) + + +def _walk_folders(folder, path=""): + try: + name = _safe_str(getattr(folder, "name", "") or "") + except Exception: + name = "" + current_path = f"{path}/{name}" if name else (path or "/") + try: + for i in range(folder.number_of_sub_messages): + try: + msg = folder.get_sub_message(i) + yield current_path, msg + except Exception: + continue + except Exception: + pass + try: + for i in range(folder.number_of_sub_folders): + try: + sub = folder.get_sub_folder(i) + yield from _walk_folders(sub, current_path) + except Exception: + continue + except Exception: + pass + + +def index_pst_file(pst_id: str, file_path: Path): + pff = None + try: + pff = pypff.file() + pff.open(str(file_path)) + root = pff.get_root_folder() + count = 0 + batch = [] + BATCH_SIZE = 500 + + with get_db() as conn: + for folder_path, msg in _walk_folders(root): + try: + batch.append(( + pst_id, folder_path, + _safe_str(getattr(msg, "subject", "")), + _extract_sender(msg), + _extract_recipients(msg), + _extract_date(msg), + _extract_body(msg), + _extract_attachments(msg), + )) + count += 1 + if len(batch) >= BATCH_SIZE: + conn.executemany( + """INSERT INTO messages + (pst_id, folder, subject, sender, recipients, sent_date, body, attachments) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + batch, + ) + conn.commit() + batch.clear() + except Exception as e: + print(f"[index] skip: {e}") + continue + + if batch: + conn.executemany( + """INSERT INTO messages + (pst_id, folder, subject, sender, recipients, sent_date, body, attachments) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + batch, + ) + conn.commit() + + conn.execute( + "UPDATE pst_files SET status = ?, message_count = ? WHERE id = ?", + ("ready", count, pst_id), + ) + conn.commit() + + print(f"[index] {pst_id}: {count} messages") + except Exception as e: + print(f"[index] failed: {e}") + with get_db() as conn: + conn.execute( + "UPDATE pst_files SET status = ?, error = ? WHERE id = ?", + ("failed", str(e), pst_id), + ) + conn.commit() + finally: + if pff is not None: + try: + pff.close() + except Exception: + pass + + +# --------------------------------------------------------------------------- +# FastAPI +# --------------------------------------------------------------------------- +app = FastAPI(title="PST Indexer") + +app.add_middleware( + CORSMiddleware, + allow_origins=[], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.on_event("startup") +def _startup(): + auth.init_auth_schema() + init_app_schema() + + +def _set_session_cookie(response: Response, token: str): + response.set_cookie( + key=SESSION_COOKIE_NAME, + value=token, + max_age=SESSION_TTL_SECONDS, + httponly=True, + secure=COOKIE_SECURE, + samesite="lax", + path="/", + ) + + +def _clear_session_cookie(response: Response): + response.delete_cookie(key=SESSION_COOKIE_NAME, path="/") + + +# ----- Setup / auth endpoints ----- + +class SetupRequest(BaseModel): + username: str + password: str + + +class LoginRequest(BaseModel): + username: str + password: str + totp_code: Optional[str] = None + + +@app.get("/api/setup/status") +def setup_status(): + """Lets the frontend decide whether to show the setup wizard.""" + return {"needs_setup": count_users() == 0} + + +@app.post("/api/setup") +def setup_admin(payload: SetupRequest, response: Response): + if count_users() > 0: + raise HTTPException(status.HTTP_409_CONFLICT, "Setup has already been completed") + + try: + user = create_user(payload.username, payload.password, role="admin") + except ValueError as e: + raise HTTPException(400, str(e)) + + record_login(user.id) + token = create_session_token(user, scope="app") + _set_session_cookie(response, token) + return {"user": user.public_dict()} + + +@app.post("/api/login") +def login(payload: LoginRequest, request: Request, response: Response): + ip = request.client.host if request.client else "unknown" + rate_key = f"{ip}:{payload.username.lower()}" + if not login_limiter.check(rate_key): + raise HTTPException(429, "Too many login attempts. Please wait a few minutes.") + + found = find_user_by_username(payload.username) + if not found: + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid credentials") + + user, stored_pw_hash = found + if not verify_password(stored_pw_hash, payload.password): + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid credentials") + + if user.mfa_enabled: + if not payload.totp_code: + # Tell the client MFA is required + return JSONResponse( + status_code=401, + content={"detail": "MFA required", "mfa_required": True}, + ) + secret = get_totp_secret(user.id) + if not secret or not verify_totp(secret, payload.totp_code): + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid MFA code") + + login_limiter.reset(rate_key) + record_login(user.id) + token = create_session_token(user, scope="app") + _set_session_cookie(response, token) + return {"user": user.public_dict()} + + +@app.post("/api/logout") +def logout(response: Response): + _clear_session_cookie(response) + return {"ok": True} + + +@app.get("/api/me") +def me(user: User = Depends(require_app_user)): + return {"user": user.public_dict()} + + +# ----- PST endpoints (all require auth) ----- + +@app.get("/api/health") +def health(): + return {"status": "ok"} + + +@app.get("/api/pst-files") +def list_pst_files(user: User = Depends(require_app_user)): + with get_db() as conn: + if user.is_admin: + rows = conn.execute( + """SELECT id, original_name, uploaded_at, size_bytes, status, + message_count, error, folder_id + FROM pst_files ORDER BY uploaded_at DESC""" + ).fetchall() + else: + allowed = allowed_folder_ids_for(user) + if allowed: + placeholders = ",".join("?" for _ in allowed) + rows = conn.execute( + f"""SELECT id, original_name, uploaded_at, size_bytes, status, + message_count, error, folder_id + FROM pst_files + WHERE folder_id IS NULL OR folder_id IN ({placeholders}) + ORDER BY uploaded_at DESC""", + list(allowed), + ).fetchall() + else: + rows = conn.execute( + """SELECT id, original_name, uploaded_at, size_bytes, status, + message_count, error, folder_id + FROM pst_files WHERE folder_id IS NULL + ORDER BY uploaded_at DESC""" + ).fetchall() + return [dict(r) for r in rows] + + +# ----- Folder endpoints (shared with admin panel) ----- + +class FolderCreate(BaseModel): + name: str + + +class FolderRename(BaseModel): + name: str + + +class FolderAssign(BaseModel): + folder_id: Optional[str] = None # null = move to uncategorized + + +def _validate_folder_name(name: str) -> str: + name = (name or "").strip() + if not name: + raise HTTPException(400, "Folder name cannot be empty") + if len(name) > 80: + raise HTTPException(400, "Folder name too long (max 80 characters)") + return name + + +@app.get("/api/folders") +def list_folders(user: User = Depends(require_app_user)): + with get_db() as conn: + if user.is_admin: + rows = conn.execute( + """SELECT f.id, f.name, f.created_at, f.created_by, + COUNT(p.id) AS file_count + FROM pst_folders f + LEFT JOIN pst_files p ON p.folder_id = f.id + GROUP BY f.id + ORDER BY f.name COLLATE NOCASE""" + ).fetchall() + else: + allowed = allowed_folder_ids_for(user) + if not allowed: + return [] + placeholders = ",".join("?" for _ in allowed) + rows = conn.execute( + f"""SELECT f.id, f.name, f.created_at, f.created_by, + COUNT(p.id) AS file_count + FROM pst_folders f + LEFT JOIN pst_files p ON p.folder_id = f.id + WHERE f.id IN ({placeholders}) + GROUP BY f.id + ORDER BY f.name COLLATE NOCASE""", + list(allowed), + ).fetchall() + return [dict(r) for r in rows] + + +@app.post("/api/folders") +def create_folder(payload: FolderCreate, user: User = Depends(require_app_user)): + # Only admins can create folders — the admin panel is the authoritative + # management surface. (We still allow creation from the main app sidebar, + # but only for admin users.) + if not user.is_admin: + raise HTTPException(403, "Only administrators can create folders") + name = _validate_folder_name(payload.name) + with get_db() as conn: + existing = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) + ).fetchone() + if existing: + raise HTTPException(409, "A folder with that name already exists") + folder_id = uuid.uuid4().hex + conn.execute( + """INSERT INTO pst_folders (id, name, created_at, created_by) + VALUES (?, ?, ?, ?)""", + (folder_id, name, datetime.now(timezone.utc).isoformat(), user.id), + ) + conn.commit() + return {"id": folder_id, "name": name, "file_count": 0} + + +@app.put("/api/folders/{folder_id}") +def rename_folder(folder_id: str, payload: FolderRename, + user: User = Depends(require_app_user)): + if not user.is_admin: + raise HTTPException(403, "Only administrators can rename folders") + name = _validate_folder_name(payload.name) + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + clash = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", + (name, folder_id), + ).fetchone() + if clash: + raise HTTPException(409, "A folder with that name already exists") + conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", + (name, folder_id)) + conn.commit() + return {"id": folder_id, "name": name} + + +@app.delete("/api/folders/{folder_id}") +def delete_folder(folder_id: str, user: User = Depends(require_app_user)): + """Delete a folder. Any PSTs inside it become uncategorized (folder_id = NULL).""" + if not user.is_admin: + raise HTTPException(403, "Only administrators can delete folders") + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", + (folder_id,)) + conn.execute("DELETE FROM folder_permissions WHERE folder_id = ?", + (folder_id,)) + conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) + conn.commit() + return {"deleted": folder_id} + + +@app.put("/api/pst-files/{pst_id}/folder") +def assign_pst_to_folder(pst_id: str, payload: FolderAssign, + user: User = Depends(require_app_user)): + # Check the user can see the PST's current location AND the destination + if not user_can_see_pst(user, pst_id): + raise HTTPException(404, "PST not found") + if payload.folder_id and not user_can_see_folder(user, payload.folder_id): + raise HTTPException(403, "You do not have access to the target folder") + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_files WHERE id = ?", + (pst_id,)).fetchone(): + raise HTTPException(404, "PST not found") + if payload.folder_id: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (payload.folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", + (payload.folder_id, pst_id)) + conn.commit() + return {"pst_id": pst_id, "folder_id": payload.folder_id} + + +@app.post("/api/pst-files") +async def upload_pst( + background_tasks: BackgroundTasks, + file: UploadFile = File(...), + folder_id: Optional[str] = None, + user: User = Depends(require_app_user), +): + if not file.filename: + raise HTTPException(400, "No filename provided") + if not file.filename.lower().endswith((".pst", ".ost")): + raise HTTPException(400, "Only .pst and .ost files are supported") + + # Validate folder if provided (and verify user has access to it) + if folder_id: + with get_db() as conn: + exists = conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone() + if not exists: + raise HTTPException(400, "Target folder does not exist") + if not user_can_see_folder(user, folder_id): + raise HTTPException(403, "You do not have access to the target folder") + + pst_id = uuid.uuid4().hex + stored_name = f"{pst_id}_{file.filename}" + stored_path = UPLOAD_DIR / stored_name + + size = 0 + async with aiofiles.open(stored_path, "wb") as out: + while chunk := await file.read(1024 * 1024): + await out.write(chunk) + size += len(chunk) + + with get_db() as conn: + conn.execute( + """INSERT INTO pst_files + (id, filename, original_name, uploaded_at, size_bytes, status, + uploaded_by, folder_id) + VALUES (?, ?, ?, ?, ?, 'indexing', ?, ?)""", + (pst_id, stored_name, file.filename, + datetime.now(timezone.utc).isoformat(), size, user.id, folder_id), + ) + conn.commit() + + background_tasks.add_task(asyncio.to_thread, index_pst_file, pst_id, stored_path) + return {"id": pst_id, "original_name": file.filename, + "status": "indexing", "size_bytes": size, "folder_id": folder_id} + + +@app.delete("/api/pst-files/{pst_id}") +def delete_pst(pst_id: str, user: User = Depends(require_app_user)): + if not user_can_see_pst(user, pst_id): + raise HTTPException(404, "PST not found") + with get_db() as conn: + row = conn.execute( + "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) + ).fetchone() + if not row: + raise HTTPException(404, "PST not found") + conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) + conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) + conn.commit() + try: + (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) + except Exception: + pass + return {"deleted": pst_id} + + +@app.get("/api/pst-files/{pst_id}/folders") +def get_folders(pst_id: str, user: User = Depends(require_app_user)): + if not user_can_see_pst(user, pst_id): + raise HTTPException(404, "PST not found") + with get_db() as conn: + rows = conn.execute( + """SELECT folder, COUNT(*) as count FROM messages + WHERE pst_id = ? GROUP BY folder ORDER BY folder""", + (pst_id,), + ).fetchall() + return [dict(r) for r in rows] + + +def _escape_fts(query: str) -> str: + tokens = [] + for raw in query.split(): + if re.fullmatch(r"[A-Za-z0-9]+", raw): + tokens.append(f'"{raw}"*') + else: + sanitized = raw.replace('"', '""') + tokens.append(f'"{sanitized}"') + return " ".join(tokens) + + +@app.get("/api/pst-files/{pst_id}/messages") +def search_messages( + pst_id: str, + q: Optional[str] = None, + folder: Optional[str] = None, + limit: int = 50, + offset: int = 0, + user: User = Depends(require_app_user), +): + if not user_can_see_pst(user, pst_id): + raise HTTPException(404, "PST not found") + limit = max(1, min(limit, 200)) + offset = max(0, offset) + + with get_db() as conn: + p = conn.execute("SELECT id FROM pst_files WHERE id = ?", (pst_id,)).fetchone() + if not p: + raise HTTPException(404, "PST not found") + + params = [pst_id] + + if q and q.strip(): + fts_q = _escape_fts(q.strip()) + base = """ + FROM messages m + JOIN messages_fts f ON f.rowid = m.id + WHERE m.pst_id = ? AND messages_fts MATCH ? + """ + params.append(fts_q) + if folder: + base += " AND m.folder = ?" + params.append(folder) + + total = conn.execute("SELECT COUNT(*) as c " + base, params).fetchone()["c"] + + data_sql = ( + "SELECT m.id, m.folder, m.subject, m.sender, m.recipients, m.sent_date, " + " m.attachments, snippet(messages_fts, 3, '', '', '…', 24) AS snippet " + + base + " ORDER BY rank LIMIT ? OFFSET ?" + ) + rows = conn.execute(data_sql, params + [limit, offset]).fetchall() + else: + where = ["m.pst_id = ?"] + if folder: + where.append("m.folder = ?") + params.append(folder) + where_sql = " AND ".join(where) + total = conn.execute( + f"SELECT COUNT(*) as c FROM messages m WHERE {where_sql}", params + ).fetchone()["c"] + rows = conn.execute( + f"""SELECT m.id, m.folder, m.subject, m.sender, m.recipients, + m.sent_date, m.attachments, substr(m.body, 1, 200) AS snippet + FROM messages m WHERE {where_sql} + ORDER BY m.sent_date DESC LIMIT ? OFFSET ?""", + params + [limit, offset], + ).fetchall() + + return {"total": total, "limit": limit, "offset": offset, + "items": [dict(r) for r in rows]} + + +@app.get("/api/messages/{message_id}") +def get_message(message_id: int, user: User = Depends(require_app_user)): + with get_db() as conn: + row = conn.execute( + """SELECT id, pst_id, folder, subject, sender, recipients, + sent_date, body, attachments + FROM messages WHERE id = ?""", + (message_id,), + ).fetchone() + if not row: + raise HTTPException(404, "Message not found") + if not user_can_see_pst(user, row["pst_id"]): + raise HTTPException(404, "Message not found") + return dict(row) + + +# --------------------------------------------------------------------------- +# Static frontend (anonymous access — but index.html enforces auth client-side +# AND the APIs enforce it server-side, which is what actually matters) +# --------------------------------------------------------------------------- +if FRONTEND_DIR.exists(): + app.mount("/", StaticFiles(directory=str(FRONTEND_DIR), html=True), name="frontend") diff --git a/OLD_VERSIONS/v2.0/pst-indexer/backend/auth.py b/OLD_VERSIONS/v2.0/pst-indexer/backend/auth.py new file mode 100644 index 0000000..eedd118 --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/backend/auth.py @@ -0,0 +1,513 @@ +""" +auth.py — Shared authentication & encryption module. + +Responsibilities: +- Derive or generate a master secret key (persisted to disk with chmod 600) +- Provide Fernet-based encryption for user PII at rest +- Argon2id password hashing (constant-time verification) +- TOTP secret generation, QR provisioning URI, 6-digit code verification +- JWT session tokens (signed with master key) +- User CRUD on the shared SQLite database +- FastAPI dependencies to enforce authentication and admin role +""" +from __future__ import annotations + +import base64 +import hashlib +import hmac +import os +import secrets +import sqlite3 +import time +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Optional + +import jwt +import pyotp +from argon2 import PasswordHasher +from argon2.exceptions import VerifyMismatchError, InvalidHashError +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from fastapi import Depends, HTTPException, Request, status + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- +DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) +DATA_DIR.mkdir(parents=True, exist_ok=True) + +SECRET_KEY_PATH = DATA_DIR / "secret.key" +DB_PATH = DATA_DIR / "pst_index.db" + +SESSION_TTL_SECONDS = int(os.environ.get("SESSION_TTL_SECONDS", 60 * 60 * 8)) # 8 hours +SESSION_COOKIE_NAME = "pst_session" +ADMIN_SESSION_COOKIE_NAME = "pst_admin_session" + +JWT_ALGORITHM = "HS256" + +# --------------------------------------------------------------------------- +# Master secret +# --------------------------------------------------------------------------- +def _load_or_create_master_secret() -> bytes: + """Return a 32-byte master secret. Created on first run, persisted to disk.""" + # Allow overriding via env var (useful for orchestration); still persist so + # restarts without the env var keep working. + env_secret = os.environ.get("MASTER_SECRET") + if env_secret: + return hashlib.sha256(env_secret.encode("utf-8")).digest() + + if SECRET_KEY_PATH.exists(): + data = SECRET_KEY_PATH.read_bytes() + if len(data) == 32: + return data + # Legacy or truncated: rewrap + return hashlib.sha256(data).digest() + + secret = secrets.token_bytes(32) + SECRET_KEY_PATH.write_bytes(secret) + try: + os.chmod(SECRET_KEY_PATH, 0o600) + except Exception: + pass + return secret + + +MASTER_SECRET: bytes = _load_or_create_master_secret() + + +def _derive_subkey(info: bytes, length: int = 32) -> bytes: + """Derive a sub-key from the master secret using HKDF-SHA256.""" + return HKDF( + algorithm=hashes.SHA256(), + length=length, + salt=b"pst-indexer-salt-v1", + info=info, + ).derive(MASTER_SECRET) + + +# Key used for encrypting user PII at rest +_FERNET_KEY = base64.urlsafe_b64encode(_derive_subkey(b"fernet-user-pii")) +_FERNET = Fernet(_FERNET_KEY) + +# Key used for JWT signatures +_JWT_KEY = _derive_subkey(b"jwt-sessions") + +# Key used for deterministic username lookup hashes (so we can index without +# exposing plaintext usernames in the DB). +_USERNAME_HMAC_KEY = _derive_subkey(b"username-lookup") + + +# --------------------------------------------------------------------------- +# Encryption helpers +# --------------------------------------------------------------------------- +def encrypt(plaintext: str) -> str: + """Fernet-encrypt a string; returns base64 ciphertext.""" + if plaintext is None: + return "" + return _FERNET.encrypt(plaintext.encode("utf-8")).decode("utf-8") + + +def decrypt(ciphertext: str) -> str: + """Fernet-decrypt. Returns empty string for empty input.""" + if not ciphertext: + return "" + try: + return _FERNET.decrypt(ciphertext.encode("utf-8")).decode("utf-8") + except InvalidToken: + raise ValueError("Failed to decrypt — master key may have changed") + + +def username_lookup_hash(username: str) -> str: + """Deterministic HMAC-SHA256 over the normalized username, hex-encoded.""" + normalized = username.strip().lower().encode("utf-8") + return hmac.new(_USERNAME_HMAC_KEY, normalized, hashlib.sha256).hexdigest() + + +# --------------------------------------------------------------------------- +# Password hashing +# --------------------------------------------------------------------------- +_hasher = PasswordHasher( + time_cost=3, + memory_cost=64 * 1024, # 64 MiB + parallelism=2, +) + + +def hash_password(password: str) -> str: + return _hasher.hash(password) + + +def verify_password(stored_hash: str, password: str) -> bool: + try: + _hasher.verify(stored_hash, password) + return True + except (VerifyMismatchError, InvalidHashError): + return False + except Exception: + return False + + +# --------------------------------------------------------------------------- +# TOTP (MFA) +# --------------------------------------------------------------------------- +def generate_totp_secret() -> str: + return pyotp.random_base32() + + +def totp_provisioning_uri(secret: str, username: str, issuer: str = "PST Archive") -> str: + return pyotp.TOTP(secret).provisioning_uri(name=username, issuer_name=issuer) + + +def verify_totp(secret: str, code: str) -> bool: + if not secret or not code: + return False + try: + return pyotp.TOTP(secret).verify(code.strip(), valid_window=1) + except Exception: + return False + + +# --------------------------------------------------------------------------- +# Database +# --------------------------------------------------------------------------- +@contextmanager +def get_db(): + conn = sqlite3.connect(str(DB_PATH)) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + try: + yield conn + finally: + conn.close() + + +def init_auth_schema(): + with get_db() as conn: + conn.executescript(""" + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username_hash TEXT NOT NULL UNIQUE, + username_enc TEXT NOT NULL, + password_hash_enc TEXT NOT NULL, + totp_secret_enc TEXT, + mfa_enabled INTEGER NOT NULL DEFAULT 0, + role TEXT NOT NULL DEFAULT 'user', + created_at TEXT NOT NULL, + last_login TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_users_username_hash ON users(username_hash); + CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); + """) + conn.commit() + + +# --------------------------------------------------------------------------- +# User model +# --------------------------------------------------------------------------- +@dataclass +class User: + id: str + username: str + role: str + mfa_enabled: bool + created_at: str + last_login: Optional[str] + + @property + def is_admin(self) -> bool: + return self.role == "admin" + + def public_dict(self) -> dict: + return { + "id": self.id, + "username": self.username, + "role": self.role, + "mfa_enabled": self.mfa_enabled, + "created_at": self.created_at, + "last_login": self.last_login, + } + + +def _row_to_user(row: sqlite3.Row) -> User: + return User( + id=row["id"], + username=decrypt(row["username_enc"]), + role=row["role"], + mfa_enabled=bool(row["mfa_enabled"]), + created_at=row["created_at"], + last_login=row["last_login"], + ) + + +def count_users() -> int: + with get_db() as conn: + return conn.execute("SELECT COUNT(*) AS c FROM users").fetchone()["c"] + + +def count_admins() -> int: + with get_db() as conn: + return conn.execute("SELECT COUNT(*) AS c FROM users WHERE role='admin'").fetchone()["c"] + + +def find_user_by_username(username: str) -> Optional[tuple[User, str]]: + """Return (User, decrypted_password_hash) if found, else None.""" + uh = username_lookup_hash(username) + with get_db() as conn: + row = conn.execute( + "SELECT * FROM users WHERE username_hash = ?", (uh,) + ).fetchone() + if not row: + return None + user = _row_to_user(row) + try: + pw_hash = decrypt(row["password_hash_enc"]) + except ValueError: + return None + return user, pw_hash + + +def find_user_by_id(user_id: str) -> Optional[User]: + with get_db() as conn: + row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() + if not row: + return None + return _row_to_user(row) + + +def get_totp_secret(user_id: str) -> Optional[str]: + with get_db() as conn: + row = conn.execute( + "SELECT totp_secret_enc FROM users WHERE id = ?", (user_id,) + ).fetchone() + if not row or not row["totp_secret_enc"]: + return None + try: + return decrypt(row["totp_secret_enc"]) + except ValueError: + return None + + +def list_users() -> list[User]: + with get_db() as conn: + rows = conn.execute( + "SELECT * FROM users ORDER BY created_at ASC" + ).fetchall() + return [_row_to_user(r) for r in rows] + + +def create_user(username: str, password: str, role: str = "user") -> User: + username = username.strip() + if not username or len(username) < 3: + raise ValueError("Username must be at least 3 characters") + if len(username) > 64: + raise ValueError("Username too long") + if role not in ("user", "admin"): + raise ValueError("Invalid role") + if len(password) < 8: + raise ValueError("Password must be at least 8 characters") + + uh = username_lookup_hash(username) + with get_db() as conn: + existing = conn.execute( + "SELECT id FROM users WHERE username_hash = ?", (uh,) + ).fetchone() + if existing: + raise ValueError("Username already taken") + + user_id = secrets.token_hex(16) + now = datetime.now(timezone.utc).isoformat() + pw_hash = hash_password(password) + + conn.execute( + """INSERT INTO users + (id, username_hash, username_enc, password_hash_enc, + totp_secret_enc, mfa_enabled, role, created_at, last_login) + VALUES (?, ?, ?, ?, NULL, 0, ?, ?, NULL)""", + (user_id, uh, encrypt(username), encrypt(pw_hash), role, now), + ) + conn.commit() + + return User( + id=user_id, username=username, role=role, mfa_enabled=False, + created_at=now, last_login=None, + ) + + +def delete_user(user_id: str) -> None: + with get_db() as conn: + # Also clean up any folder permission grants this user had + conn.execute("DELETE FROM folder_permissions WHERE user_id = ?", (user_id,)) + conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) + conn.commit() + + +def set_user_role(user_id: str, role: str) -> None: + if role not in ("user", "admin"): + raise ValueError("Invalid role") + with get_db() as conn: + conn.execute("UPDATE users SET role = ? WHERE id = ?", (role, user_id)) + conn.commit() + + +def update_password(user_id: str, new_password: str) -> None: + if len(new_password) < 8: + raise ValueError("Password must be at least 8 characters") + pw_hash = hash_password(new_password) + with get_db() as conn: + conn.execute( + "UPDATE users SET password_hash_enc = ? WHERE id = ?", + (encrypt(pw_hash), user_id), + ) + conn.commit() + + +def begin_mfa_enrollment(user_id: str) -> tuple[str, str]: + """Generate a new TOTP secret (not yet enabled) and return (secret, provisioning_uri).""" + user = find_user_by_id(user_id) + if not user: + raise ValueError("User not found") + secret = generate_totp_secret() + # Stash it but leave mfa_enabled = 0 until confirmed + with get_db() as conn: + conn.execute( + "UPDATE users SET totp_secret_enc = ?, mfa_enabled = 0 WHERE id = ?", + (encrypt(secret), user_id), + ) + conn.commit() + uri = totp_provisioning_uri(secret, user.username) + return secret, uri + + +def confirm_mfa_enrollment(user_id: str, code: str) -> bool: + secret = get_totp_secret(user_id) + if not secret or not verify_totp(secret, code): + return False + with get_db() as conn: + conn.execute("UPDATE users SET mfa_enabled = 1 WHERE id = ?", (user_id,)) + conn.commit() + return True + + +def disable_mfa(user_id: str) -> None: + with get_db() as conn: + conn.execute( + "UPDATE users SET mfa_enabled = 0, totp_secret_enc = NULL WHERE id = ?", + (user_id,), + ) + conn.commit() + + +def record_login(user_id: str) -> None: + with get_db() as conn: + conn.execute( + "UPDATE users SET last_login = ? WHERE id = ?", + (datetime.now(timezone.utc).isoformat(), user_id), + ) + conn.commit() + + +# --------------------------------------------------------------------------- +# JWT sessions +# --------------------------------------------------------------------------- +def create_session_token(user: User, scope: str = "app") -> str: + """scope='app' for main app, scope='admin' for admin panel.""" + now = datetime.now(timezone.utc) + payload = { + "sub": user.id, + "role": user.role, + "scope": scope, + "iat": int(now.timestamp()), + "exp": int((now + timedelta(seconds=SESSION_TTL_SECONDS)).timestamp()), + } + return jwt.encode(payload, _JWT_KEY, algorithm=JWT_ALGORITHM) + + +def verify_session_token(token: str, expected_scope: str) -> Optional[dict]: + try: + payload = jwt.decode(token, _JWT_KEY, algorithms=[JWT_ALGORITHM]) + if payload.get("scope") != expected_scope: + return None + return payload + except jwt.PyJWTError: + return None + + +# --------------------------------------------------------------------------- +# FastAPI dependencies +# --------------------------------------------------------------------------- +def _extract_token(request: Request, cookie_name: str) -> Optional[str]: + # Prefer cookie, fall back to Authorization header + token = request.cookies.get(cookie_name) + if token: + return token + auth = request.headers.get("authorization", "") + if auth.lower().startswith("bearer "): + return auth[7:].strip() + return None + + +def require_user(scope: str, cookie_name: str): + def _dep(request: Request) -> User: + token = _extract_token(request, cookie_name) + if not token: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated") + payload = verify_session_token(token, expected_scope=scope) + if not payload: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid or expired session") + user = find_user_by_id(payload["sub"]) + if not user: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User no longer exists") + return user + return _dep + + +def require_admin(scope: str, cookie_name: str): + base = require_user(scope, cookie_name) + def _dep(request: Request) -> User: + user = base(request) + if not user.is_admin: + raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin role required") + return user + return _dep + + +# Pre-wired dependencies for the two app scopes +require_app_user = require_user("app", SESSION_COOKIE_NAME) +require_admin_user = require_admin("admin", ADMIN_SESSION_COOKIE_NAME) + + +# --------------------------------------------------------------------------- +# Simple rate limiter (per-IP, per-username) to slow brute-force attempts +# --------------------------------------------------------------------------- +class RateLimiter: + def __init__(self, window_seconds: int = 300, max_attempts: int = 10): + self.window = window_seconds + self.max = max_attempts + self._attempts: dict[str, list[float]] = {} + + def check(self, key: str) -> bool: + now = time.time() + cutoff = now - self.window + arr = [t for t in self._attempts.get(key, []) if t > cutoff] + if len(arr) >= self.max: + self._attempts[key] = arr + return False + return True + + def record(self, key: str) -> None: + now = time.time() + cutoff = now - self.window + arr = [t for t in self._attempts.get(key, []) if t > cutoff] + arr.append(now) + self._attempts[key] = arr + + def reset(self, key: str) -> None: + self._attempts.pop(key, None) + + +login_limiter = RateLimiter(window_seconds=300, max_attempts=10) diff --git a/OLD_VERSIONS/v2.0/pst-indexer/backend/requirements.txt b/OLD_VERSIONS/v2.0/pst-indexer/backend/requirements.txt new file mode 100644 index 0000000..9cbd286 --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/backend/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.115.0 +uvicorn[standard]==0.32.0 +python-multipart==0.0.12 +libpff-python==20231205 +aiofiles==24.1.0 +argon2-cffi==23.1.0 +pyotp==2.9.0 +cryptography==43.0.1 +PyJWT==2.9.0 +qrcode[pil]==7.4.2 diff --git a/OLD_VERSIONS/v2.0/pst-indexer/docker-compose.yml b/OLD_VERSIONS/v2.0/pst-indexer/docker-compose.yml new file mode 100644 index 0000000..61385c9 --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/docker-compose.yml @@ -0,0 +1,20 @@ +services: + pst-indexer: + build: . + container_name: pst-indexer + ports: + - "8000:8000" # Main app + - "8001:8001" # Admin panel + volumes: + - ./data:/app/data + restart: unless-stopped + environment: + - DATA_DIR=/app/data + # Set to "true" when serving over HTTPS + - COOKIE_SECURE=false + # Session lifetime in seconds (default 8 hours) + - SESSION_TTL_SECONDS=28800 + # Optional: provide your own master secret. If set, this becomes the + # root key for all encryption. If omitted, a random key is generated + # on first run and persisted in ./data/secret.key (chmod 600). + # - MASTER_SECRET=change-me-to-a-long-random-string diff --git a/OLD_VERSIONS/v2.0/pst-indexer/entrypoint.sh b/OLD_VERSIONS/v2.0/pst-indexer/entrypoint.sh new file mode 100644 index 0000000..fb9fe33 --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/bash +# Start both the main app (port 8000) and the admin panel (port 8001). +# If either exits, kill the whole container so Docker's restart policy fires. + +set -e + +cleanup() { + echo "Shutting down…" + kill -TERM "$MAIN_PID" "$ADMIN_PID" 2>/dev/null || true + wait + exit 0 +} +trap cleanup SIGINT SIGTERM + +cd /app/backend + +echo "[start] Main app on :8000" +uvicorn app:app --host 0.0.0.0 --port 8000 --proxy-headers & +MAIN_PID=$! + +echo "[start] Admin panel on :8001" +uvicorn admin_app:app --host 0.0.0.0 --port 8001 --proxy-headers & +ADMIN_PID=$! + +# Wait for either to exit +wait -n "$MAIN_PID" "$ADMIN_PID" +EXIT_CODE=$? +echo "[exit] One of the services exited with code $EXIT_CODE; stopping the other" +kill -TERM "$MAIN_PID" "$ADMIN_PID" 2>/dev/null || true +wait +exit $EXIT_CODE diff --git a/OLD_VERSIONS/v2.0/pst-indexer/frontend/app.js b/OLD_VERSIONS/v2.0/pst-indexer/frontend/app.js new file mode 100644 index 0000000..fdc6cd8 --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/frontend/app.js @@ -0,0 +1,869 @@ +/* ============================================================ + PST Archive — frontend logic + - Setup wizard (first run) + - Login (+ MFA) + - Main app: upload, index, search, view + ============================================================ */ + +const API = "/api"; + +const state = { + activePstId: null, + activePstName: "", + folder: "", + query: "", + limit: 25, + offset: 0, + total: 0, + refreshTimer: null, + user: null, + folders: [], // [{id, name, file_count}, ...] + psts: [], // last loaded pst list + collapsedFolders: new Set(), // folder IDs (or "__uncat__") that are collapsed + movePstId: null, // currently being moved +}; + +const $ = (id) => document.getElementById(id); + +// ---------- helpers ---------- +function fmtBytes(n) { + if (!n) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + let i = 0; + while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } + return `${n.toFixed(i ? 1 : 0)} ${units[i]}`; +} + +function fmtDate(iso) { + if (!iso) return "—"; + try { + const d = new Date(iso); + if (isNaN(d)) return iso; + return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }); + } catch { return iso; } +} + +function fmtDateTime(iso) { + if (!iso) return "—"; + try { + const d = new Date(iso); + if (isNaN(d)) return iso; + return d.toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); + } catch { return iso; } +} + +function toast(msg, ms = 3000) { + const el = $("toast"); + el.textContent = msg; + el.classList.remove("hidden"); + clearTimeout(toast._t); + toast._t = setTimeout(() => el.classList.add("hidden"), ms); +} + +function escapeHtml(s) { + if (s == null) return ""; + return String(s) + .replace(/&/g, "&").replace(//g, ">") + .replace(/"/g, """).replace(/'/g, "'"); +} + +function renderSnippet(snippet) { + if (!snippet) return ""; + return escapeHtml(snippet) + .replace(/<mark>/g, "") + .replace(/<\/mark>/g, ""); +} + +function debounce(fn, ms) { + let t; + return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; +} + +// ---------- API ---------- +async function api(path, opts = {}) { + const res = await fetch(API + path, { + credentials: "same-origin", + ...opts, + }); + let body = null; + try { body = await res.json(); } catch {} + if (!res.ok) { + const err = new Error((body && body.detail) || res.statusText); + err.status = res.status; + err.body = body || {}; + throw err; + } + return body; +} + +// ---------- Screen switching ---------- +function showScreen(screen) { + ["setupScreen", "loginScreen", "mainApp"].forEach((id) => { + $(id).classList.toggle("hidden", id !== screen); + }); +} + +// ---------- Bootstrap ---------- +async function bootstrap() { + // 1. Try to fetch current user + try { + const { user } = await api("/me"); + state.user = user; + enterApp(); + return; + } catch (e) { + if (e.status !== 401) { + showLogin(); + toast("Connection problem — please retry."); + return; + } + } + + // 2. Not logged in — check setup status + try { + const { needs_setup } = await api("/setup/status"); + if (needs_setup) showSetup(); + else showLogin(); + } catch { + showLogin(); + } +} + +// ---------- Setup ---------- +function showSetup() { + showScreen("setupScreen"); + $("setupForm").reset(); + $("setupError").classList.add("hidden"); + setTimeout(() => $("setupForm").username?.focus(), 50); +} + +async function handleSetup(e) { + e.preventDefault(); + const form = e.target; + const errEl = $("setupError"); + errEl.classList.add("hidden"); + + const username = form.username.value.trim(); + const password = form.password.value; + const password2 = form.password2.value; + + if (password !== password2) { + errEl.textContent = "Passwords do not match."; + errEl.classList.remove("hidden"); + return; + } + + try { + const { user } = await api("/setup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + state.user = user; + toast("Welcome — admin account created."); + enterApp(); + } catch (err) { + errEl.textContent = err.message || "Setup failed."; + errEl.classList.remove("hidden"); + } +} + +// ---------- Login ---------- +function showLogin() { + showScreen("loginScreen"); + $("loginForm").reset(); + $("mfaField").classList.add("hidden"); + $("loginError").classList.add("hidden"); + setTimeout(() => $("loginForm").username?.focus(), 50); +} + +async function handleLogin(e) { + e.preventDefault(); + const form = e.target; + const errEl = $("loginError"); + errEl.classList.add("hidden"); + + const payload = { + username: form.username.value.trim(), + password: form.password.value, + }; + const mfaInput = form.totp_code.value.trim(); + if (mfaInput) payload.totp_code = mfaInput; + + try { + const { user } = await api("/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + state.user = user; + toast(`Welcome, ${user.username}.`); + enterApp(); + } catch (err) { + if (err.status === 401 && err.body && err.body.mfa_required) { + $("mfaField").classList.remove("hidden"); + const f = $("loginForm").totp_code; + f.value = ""; + setTimeout(() => f.focus(), 50); + errEl.textContent = "Enter your authenticator code to continue."; + errEl.classList.remove("hidden"); + return; + } + errEl.textContent = err.message || "Login failed."; + errEl.classList.remove("hidden"); + } +} + +async function handleLogout() { + try { await api("/logout", { method: "POST" }); } catch {} + state.user = null; + state.activePstId = null; + resetReader(); + showLogin(); +} + +// ---------- Enter the app ---------- +function enterApp() { + showScreen("mainApp"); + $("userChip").textContent = `${state.user.username}${state.user.role === "admin" ? " · admin" : ""}`; + checkHealth(); + loadPstList(); + setInterval(checkHealth, 30000); +} + +// ---------- Health ---------- +async function checkHealth() { + const dot = $("statusDot"); + const text = $("statusText"); + try { + await api("/health"); + dot.className = "status-dot ok"; + text.textContent = "connected"; + } catch { + dot.className = "status-dot err"; + text.textContent = "offline"; + } +} + +// ---------- PST list ---------- +async function loadPstList() { + try { + const [folders, items] = await Promise.all([ + api("/folders"), + api("/pst-files"), + ]); + state.folders = folders; + state.psts = items; + renderPstList(); + if (items.some((i) => i.status === "indexing")) { + clearTimeout(state.refreshTimer); + state.refreshTimer = setTimeout(loadPstList, 2500); + } + } catch (e) { + if (e.status === 401) { handleLogout(); return; } + toast(`Failed to load archives: ${e.message}`); + } +} + +function pstItemHtml(i) { + const active = i.id === state.activePstId ? "active" : ""; + const disabled = i.status !== "ready"; + const statusText = i.status === "indexing" ? "indexing…" : + i.status === "ready" ? `${i.message_count.toLocaleString()} msgs` : + i.status === "failed" ? "failed" : i.status; + return ` +
  • +
    + ${escapeHtml(i.original_name)} + +
    +
    + ${fmtBytes(i.size_bytes)} · ${fmtDate(i.uploaded_at)} + ${statusText} +
    +
  • + `; +} + +function renderPstList() { + const ul = $("pstList"); + const { folders, psts } = state; + + if (!folders.length && !psts.length) { + ul.innerHTML = '
  • No archives yet.
  • '; + return; + } + + // Group PSTs by folder_id + const byFolder = new Map(); + byFolder.set(null, []); // uncategorized bucket + folders.forEach((f) => byFolder.set(f.id, [])); + psts.forEach((p) => { + const bucket = byFolder.has(p.folder_id) ? p.folder_id : null; + byFolder.get(bucket).push(p); + }); + + const parts = []; + + // Render user-defined folders first (sorted alphabetically — server does this) + for (const f of folders) { + const items = byFolder.get(f.id) || []; + const collapsed = state.collapsedFolders.has(f.id) ? "collapsed" : ""; + parts.push(` +
  • +
    +
    + + ${escapeHtml(f.name)} + ${items.length} +
    + +
    +
      + ${items.length + ? items.map(pstItemHtml).join("") + : '
    • — empty —
    • '} +
    +
  • + `); + } + + // Uncategorized group — only shown if it has items OR there are no folders + const uncat = byFolder.get(null) || []; + if (uncat.length || !folders.length) { + const collapsed = state.collapsedFolders.has("__uncat__") ? "collapsed" : ""; + parts.push(` +
  • +
    +
    + + Uncategorized + ${uncat.length} +
    +
    +
      + ${uncat.length + ? uncat.map(pstItemHtml).join("") + : '
    • — empty —
    • '} +
    +
  • + `); + } + + ul.innerHTML = parts.join(""); + + // PST click + ul.querySelectorAll(".pst-item").forEach((el) => { + el.addEventListener("click", (e) => { + if (e.target.closest("[data-pst-menu]")) return; + if (el.dataset.disabled) { + toast(el.dataset.status === "indexing" ? "Still indexing — one moment…" : "This archive is unavailable."); + return; + } + selectPst(el.dataset.id, el.dataset.name); + }); + }); + + // PST action menu + ul.querySelectorAll("[data-pst-menu]").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + openPstMenu(e, btn.dataset.pstMenu); + }); + }); + + // Folder header toggle (collapse/expand) + ul.querySelectorAll("[data-folder-header]").forEach((el) => { + el.addEventListener("click", (e) => { + if (e.target.closest("[data-folder-menu]")) return; + const id = el.dataset.folderHeader; + if (state.collapsedFolders.has(id)) state.collapsedFolders.delete(id); + else state.collapsedFolders.add(id); + const group = el.closest(".folder-group"); + group.classList.toggle("collapsed"); + }); + }); + + // Folder action menu + ul.querySelectorAll("[data-folder-menu]").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + openFolderMenu(e, btn.dataset.folderMenu); + }); + }); +} + +// ---------- Context menus ---------- +function closeContextMenu() { + document.querySelectorAll(".context-menu").forEach((el) => el.remove()); +} + +function openContextMenu(x, y, items) { + closeContextMenu(); + const menu = document.createElement("div"); + menu.className = "context-menu"; + items.forEach((it) => { + if (it.divider) { + menu.appendChild(document.createElement("hr")); + return; + } + const btn = document.createElement("button"); + btn.textContent = it.label; + if (it.danger) btn.classList.add("danger"); + btn.addEventListener("click", () => { + closeContextMenu(); + it.onClick(); + }); + menu.appendChild(btn); + }); + document.body.appendChild(menu); + + // Position, keeping menu inside viewport + const rect = menu.getBoundingClientRect(); + const maxX = window.innerWidth - rect.width - 4; + const maxY = window.innerHeight - rect.height - 4; + menu.style.left = Math.min(x, maxX) + "px"; + menu.style.top = Math.min(y, maxY) + "px"; + + // Close on outside click or escape + setTimeout(() => { + document.addEventListener("click", closeContextMenu, { once: true }); + }, 0); +} + +function openPstMenu(event, pstId) { + const p = state.psts.find((x) => x.id === pstId); + if (!p) return; + const r = event.currentTarget.getBoundingClientRect(); + openContextMenu(r.left, r.bottom + 4, [ + { + label: "Move to folder…", + onClick: () => openMoveModal(p), + }, + { divider: true }, + { + label: "Delete archive", + danger: true, + onClick: () => deletePst(p), + }, + ]); +} + +function openFolderMenu(event, folderId) { + const f = state.folders.find((x) => x.id === folderId); + if (!f) return; + const r = event.currentTarget.getBoundingClientRect(); + openContextMenu(r.left, r.bottom + 4, [ + { + label: "Rename folder…", + onClick: () => renameFolder(f), + }, + { divider: true }, + { + label: "Delete folder", + danger: true, + onClick: () => deleteFolder(f), + }, + ]); +} + +// ---------- Folder operations ---------- +function openFolderModal() { + $("folderForm").reset(); + $("folderError").classList.add("hidden"); + $("folderModal").classList.remove("hidden"); + setTimeout(() => $("folderForm").name.focus(), 50); +} + +async function handleCreateFolder(e) { + e.preventDefault(); + const form = e.target; + const errEl = $("folderError"); + errEl.classList.add("hidden"); + try { + await api("/folders", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: form.name.value }), + }); + toast("Folder created."); + closeModals(); + loadPstList(); + } catch (err) { + errEl.textContent = err.message || "Create failed."; + errEl.classList.remove("hidden"); + } +} + +async function renameFolder(f) { + const newName = prompt(`Rename "${f.name}" to:`, f.name); + if (!newName || newName.trim() === f.name) return; + try { + await api(`/folders/${f.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: newName }), + }); + toast("Folder renamed."); + loadPstList(); + } catch (err) { + toast(`Rename failed: ${err.message}`); + } +} + +async function deleteFolder(f) { + if (!confirm(`Delete folder "${f.name}"?\n\nAny archives inside will be moved to Uncategorized.`)) return; + try { + await api(`/folders/${f.id}`, { method: "DELETE" }); + toast("Folder deleted."); + loadPstList(); + } catch (err) { + toast(`Delete failed: ${err.message}`); + } +} + +async function deletePst(p) { + if (!confirm(`Remove "${p.original_name}" and all indexed messages?`)) return; + try { + await api(`/pst-files/${p.id}`, { method: "DELETE" }); + if (state.activePstId === p.id) { state.activePstId = null; resetReader(); } + toast("Archive removed."); + loadPstList(); + } catch (err) { + toast(`Delete failed: ${err.message}`); + } +} + +function openMoveModal(p) { + state.movePstId = p.id; + $("moveSubject").textContent = p.original_name; + + const sel = $("moveFolderSelect"); + sel.innerHTML = '' + + state.folders.map((f) => + `` + ).join(""); + if (!p.folder_id) sel.value = ""; + + $("moveError").classList.add("hidden"); + $("moveModal").classList.remove("hidden"); +} + +async function handleMovePst(e) { + e.preventDefault(); + const errEl = $("moveError"); + errEl.classList.add("hidden"); + const folderId = $("moveFolderSelect").value || null; + try { + await api(`/pst-files/${state.movePstId}/folder`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ folder_id: folderId }), + }); + toast("Moved."); + closeModals(); + loadPstList(); + } catch (err) { + errEl.textContent = err.message || "Move failed."; + errEl.classList.remove("hidden"); + } +} + +function closeModals() { + document.querySelectorAll(".modal").forEach((m) => m.classList.add("hidden")); +} + +// ---------- Selection + folders ---------- +async function selectPst(id, name) { + state.activePstId = id; + state.activePstName = name; + state.folder = ""; + state.query = ""; + state.offset = 0; + + $("searchInput").value = ""; + $("searchInput").disabled = false; + $("folderFilter").disabled = false; + + document.querySelectorAll(".pst-item").forEach((el) => { + el.classList.toggle("active", el.dataset.id === id); + }); + $("readerTitle").textContent = name; + $("readerMeta").textContent = "Loading messages…"; + hideMessageView(); + await Promise.all([loadFolders(), loadMessages()]); +} + +async function loadFolders() { + if (!state.activePstId) return; + try { + const folders = await api(`/pst-files/${state.activePstId}/folders`); + const sel = $("folderFilter"); + sel.innerHTML = + '' + + folders.map((f) => ``).join(""); + sel.value = state.folder || ""; + } catch (e) { console.warn("folders", e); } +} + +// ---------- Messages ---------- +async function loadMessages() { + if (!state.activePstId) return; + const params = new URLSearchParams(); + if (state.query) params.set("q", state.query); + if (state.folder) params.set("folder", state.folder); + params.set("limit", state.limit); + params.set("offset", state.offset); + + try { + const res = await api(`/pst-files/${state.activePstId}/messages?${params}`); + state.total = res.total; + renderMessages(res.items); + renderPaginator(); + $("readerMeta").textContent = + `${state.total.toLocaleString()} message${state.total === 1 ? "" : "s"}` + + (state.query ? ` matching "${state.query}"` : "") + + (state.folder ? ` in ${state.folder}` : ""); + } catch (e) { + if (e.status === 401) { handleLogout(); return; } + toast(`Search failed: ${e.message}`); + } +} + +function renderMessages(items) { + const list = $("messageList"); + if (!items.length) { + list.innerHTML = ` +
    +
    +

    No messages match.

    +
    `; + return; + } + list.innerHTML = items.map((m) => { + const hasAtt = m.attachments && m.attachments.trim().length > 0; + return ` +
    +
    ${escapeHtml(m.sender || "(unknown sender)")}
    +
    +
    ${escapeHtml(m.subject || "(no subject)")} ${hasAtt ? '' : ""}
    +
    ${renderSnippet(m.snippet || "")}
    +
    +
    +
    ${fmtDate(m.sent_date)}
    +
    ${escapeHtml((m.folder || "").split("/").filter(Boolean).pop() || "inbox")}
    +
    +
    `; + }).join(""); + list.querySelectorAll(".message-row").forEach((el) => { + el.addEventListener("click", () => openMessage(el.dataset.id)); + }); +} + +function renderPaginator() { + const p = $("paginator"); + if (state.total <= state.limit) { p.classList.add("hidden"); return; } + p.classList.remove("hidden"); + const from = state.offset + 1; + const to = Math.min(state.offset + state.limit, state.total); + $("pageInfo").textContent = `${from}–${to} of ${state.total.toLocaleString()}`; + $("prevBtn").disabled = state.offset === 0; + $("nextBtn").disabled = state.offset + state.limit >= state.total; +} + +// ---------- Message view ---------- +async function openMessage(id) { + try { + const m = await api(`/messages/${id}`); + $("mvSubject").textContent = m.subject || "(no subject)"; + $("mvSender").textContent = m.sender || "—"; + $("mvRecipients").textContent = m.recipients || "—"; + $("mvDate").textContent = fmtDateTime(m.sent_date); + $("mvFolder").textContent = m.folder || "—"; + if (m.attachments && m.attachments.trim()) { + $("mvAttLabel").classList.remove("hidden"); + $("mvAttachments").classList.remove("hidden"); + $("mvAttachments").textContent = m.attachments.split(" | ").join(", "); + } else { + $("mvAttLabel").classList.add("hidden"); + $("mvAttachments").classList.add("hidden"); + } + $("mvBody").textContent = m.body || "(empty body)"; + $("messageView").classList.remove("hidden"); + } catch (e) { + if (e.status === 401) { handleLogout(); return; } + toast(`Could not open message: ${e.message}`); + } +} + +function hideMessageView() { $("messageView").classList.add("hidden"); } + +function resetReader() { + $("readerTitle").textContent = "Select an archive"; + $("readerMeta").textContent = "Upload a PST file or select one from the left to begin."; + $("searchInput").disabled = true; + $("folderFilter").disabled = true; + $("messageList").innerHTML = ` +
    +
    +

    The archive awaits.

    +
    `; + $("paginator").classList.add("hidden"); + hideMessageView(); +} + +// ---------- Upload ---------- +function uploadFile(file) { + if (!file) return; + const lower = file.name.toLowerCase(); + if (!lower.endsWith(".pst") && !lower.endsWith(".ost")) { + toast("Only .pst and .ost files are accepted."); + return; + } + const progress = $("uploadProgress"); + const bar = $("progressBar"); + const pct = $("progressPct"); + const nm = $("progressName"); + progress.classList.remove("hidden"); + nm.textContent = file.name; + bar.style.width = "0%"; + pct.textContent = "0%"; + + const form = new FormData(); + form.append("file", file); + + const xhr = new XMLHttpRequest(); + xhr.open("POST", API + "/pst-files"); + xhr.withCredentials = true; + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) { + const p = Math.round((e.loaded / e.total) * 100); + bar.style.width = p + "%"; + pct.textContent = p + "%"; + } + }; + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + pct.textContent = "indexing…"; + bar.style.width = "100%"; + toast("Upload complete. Indexing started in background."); + setTimeout(() => progress.classList.add("hidden"), 2500); + loadPstList(); + } else if (xhr.status === 401) { + handleLogout(); + } else { + let msg = "Upload failed"; + try { msg = JSON.parse(xhr.responseText).detail || msg; } catch {} + toast(msg); + progress.classList.add("hidden"); + } + }; + xhr.onerror = () => { toast("Upload failed (network)."); progress.classList.add("hidden"); }; + xhr.send(form); +} + +// ---------- Theme ---------- +function initTheme() { + // Preference order: localStorage → prefers-color-scheme → light + let theme = "light"; + try { + const saved = localStorage.getItem("pst-theme"); + if (saved === "light" || saved === "dark") { + theme = saved; + } else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { + theme = "dark"; + } + } catch { /* ignore storage errors */ } + document.documentElement.dataset.theme = theme; +} + +function toggleTheme() { + const current = document.documentElement.dataset.theme === "dark" ? "dark" : "light"; + const next = current === "dark" ? "light" : "dark"; + document.documentElement.dataset.theme = next; + try { localStorage.setItem("pst-theme", next); } catch {} +} + +// Apply theme immediately so there's no flash of wrong theme +initTheme(); + +// ---------- Wire up ---------- +function init() { + $("setupForm").addEventListener("submit", handleSetup); + $("loginForm").addEventListener("submit", handleLogin); + $("logoutBtn").addEventListener("click", handleLogout); + + // Theme toggles (both the in-masthead and floating variants) + ["themeToggle", "themeToggleFloat"].forEach((id) => { + const el = document.getElementById(id); + if (el) el.addEventListener("click", toggleTheme); + }); + + $("fileInput").addEventListener("change", (e) => { + const f = e.target.files[0]; + if (f) uploadFile(f); + e.target.value = ""; + }); + + const dz = $("dropzone"); + ["dragenter", "dragover"].forEach((ev) => { + dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.add("drag-over"); }); + }); + ["dragleave", "drop"].forEach((ev) => { + dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.remove("drag-over"); }); + }); + dz.addEventListener("drop", (e) => { + const f = e.dataTransfer.files?.[0]; + if (f) uploadFile(f); + }); + + $("refreshBtn").addEventListener("click", loadPstList); + $("newFolderBtn").addEventListener("click", openFolderModal); + $("folderForm").addEventListener("submit", handleCreateFolder); + $("moveForm").addEventListener("submit", handleMovePst); + + // Modal close buttons + backdrop clicks + Escape + document.querySelectorAll("[data-close]").forEach((el) => { + el.addEventListener("click", closeModals); + }); + document.querySelectorAll(".modal").forEach((m) => { + m.addEventListener("click", (e) => { + if (e.target === m) closeModals(); + }); + }); + + const searchHandler = debounce(() => { + state.query = $("searchInput").value.trim(); + state.offset = 0; + loadMessages(); + }, 250); + $("searchInput").addEventListener("input", searchHandler); + + $("folderFilter").addEventListener("change", () => { + state.folder = $("folderFilter").value; + state.offset = 0; + loadMessages(); + }); + + $("prevBtn").addEventListener("click", () => { + state.offset = Math.max(0, state.offset - state.limit); + loadMessages(); + }); + $("nextBtn").addEventListener("click", () => { + state.offset += state.limit; + loadMessages(); + }); + + $("closeMessageBtn").addEventListener("click", hideMessageView); + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + hideMessageView(); + closeModals(); + closeContextMenu(); + } + }); + + bootstrap(); +} + +document.addEventListener("DOMContentLoaded", init); diff --git a/OLD_VERSIONS/v2.0/pst-indexer/frontend/index.html b/OLD_VERSIONS/v2.0/pst-indexer/frontend/index.html new file mode 100644 index 0000000..1e0be42 --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/frontend/index.html @@ -0,0 +1,256 @@ + + + + + +PST Archive · Mail Indexer + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/OLD_VERSIONS/v2.0/pst-indexer/frontend/styles.css b/OLD_VERSIONS/v2.0/pst-indexer/frontend/styles.css new file mode 100644 index 0000000..8aa34f3 --- /dev/null +++ b/OLD_VERSIONS/v2.0/pst-indexer/frontend/styles.css @@ -0,0 +1,1136 @@ +/* ============================================================ + PST ARCHIVE — aesthetic: editorial/forensic archive + Warm paper palette, serif display, mono accents, grain overlay + Supports light (default) and dark themes via [data-theme] + ============================================================ */ + +:root, +:root[data-theme="light"] { + --paper: #f2ede3; + --paper-deep: #e8e1d3; + --ink: #1b1814; + --ink-soft: #3a332a; + --ink-muted: #6b6258; + --rule: #c9bfad; + --rule-soft: #dcd3c1; + --accent: #a8341f; /* oxblood red */ + --accent-soft: #c95a42; + --highlight: #e8d67a; /* faded yellow marker */ + --ok: #2e6f3a; + --warn: #a86b1e; + --shadow: 0 1px 0 rgba(27, 24, 20, 0.04), 0 20px 40px -20px rgba(27, 24, 20, 0.25); + --grain-opacity: 0.35; + + --font-display: "Fraunces", "Iowan Old Style", "Palatino", Georgia, serif; + --font-mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, monospace; +} + +:root[data-theme="dark"] { + --paper: #17140f; /* deep inked paper */ + --paper-deep: #201c16; /* slightly lighter ink-pool for surfaces */ + --ink: #e9e0cb; /* warm paper-cream as the ink color */ + --ink-soft: #c4b99f; + --ink-muted: #8a8170; + --rule: #3a3226; + --rule-soft: #2a251d; + --accent: #d06347; /* softened oxblood that holds up on dark */ + --accent-soft: #e4876c; + --highlight: #d8b64a; /* brass-yellow marker */ + --ok: #6fae7c; + --warn: #d99a4a; + --shadow: 0 1px 0 rgba(0, 0, 0, 0.4), 0 20px 40px -20px rgba(0, 0, 0, 0.9); + --grain-opacity: 0.12; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + height: 100%; + background: var(--paper); + color: var(--ink); + font-family: var(--font-display); + font-feature-settings: "ss01", "onum"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; + background-image: + radial-gradient(circle at 10% 10%, rgba(168, 52, 31, 0.04), transparent 40%), + radial-gradient(circle at 90% 90%, rgba(74, 58, 42, 0.05), transparent 50%); +} + +:root[data-theme="dark"] body { + background-image: + radial-gradient(circle at 10% 10%, rgba(208, 99, 71, 0.06), transparent 40%), + radial-gradient(circle at 90% 90%, rgba(216, 182, 74, 0.03), transparent 50%); +} + +/* grain overlay */ +.grain { + pointer-events: none; + position: fixed; + inset: 0; + z-index: 9999; + opacity: var(--grain-opacity); + mix-blend-mode: multiply; + background-image: url("data:image/svg+xml;utf8,"); +} + +:root[data-theme="dark"] .grain { + mix-blend-mode: screen; +} + +/* ============================================================ + MASTHEAD + ============================================================ */ +.masthead { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 28px 40px 18px; + border-bottom: 1px solid var(--ink); + position: relative; +} + +.masthead::after { + content: ""; + position: absolute; + bottom: -4px; + left: 40px; + right: 40px; + height: 1px; + background: var(--ink); +} + +.masthead-left { display: flex; align-items: center; gap: 18px; } + +.logo-mark { + color: var(--ink); + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border: 1px solid var(--ink); + background: var(--paper); +} + +.masthead-title h1 { + margin: 0; + font-size: 2.5rem; + font-weight: 500; + letter-spacing: -0.02em; + line-height: 1; + font-variation-settings: "SOFT" 30, "WONK" 1; +} + +.tagline { + margin: 6px 0 0; + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.masthead-right { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--warn); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 25%, transparent); +} +.status-dot.ok { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 25%, transparent); } +.status-dot.err { background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); } + +/* ============================================================ + WORKSPACE + ============================================================ */ +.workspace { + display: grid; + grid-template-columns: 340px 1fr; + flex: 1; + min-height: 0; +} + +/* ============================================================ + SIDEBAR + ============================================================ */ +.sidebar { + border-right: 1px solid var(--rule); + padding: 28px 24px; + display: flex; + flex-direction: column; + gap: 36px; + overflow-y: auto; + background: linear-gradient(180deg, rgba(232, 225, 211, 0.4), transparent 40%); +} + +.section-heading { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ink-soft); + margin: 0 0 14px; + padding-bottom: 8px; + border-bottom: 1px solid var(--rule); + display: flex; + align-items: baseline; + justify-content: space-between; +} + +.dropzone { + display: block; + border: 1.5px dashed var(--ink-muted); + background: var(--paper-deep); + padding: 28px 18px; + text-align: center; + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} +.dropzone:hover, +.dropzone.drag-over { + border-color: var(--accent); + background: var(--rule-soft); + transform: translateY(-1px); +} +.dropzone-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + color: var(--ink-soft); +} +.dropzone-title { + font-family: var(--font-display); + font-size: 1.1rem; + font-style: italic; + font-weight: 400; +} +.dropzone-title em { + font-style: normal; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--accent); +} +.dropzone-hint { + font-family: var(--font-mono); + font-size: 0.65rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.upload-progress { margin-top: 14px; } +.progress-label { + display: flex; + justify-content: space-between; + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--ink-muted); + margin-bottom: 6px; +} +.progress-track { + height: 4px; + background: var(--rule-soft); + overflow: hidden; +} +.progress-bar { + height: 100%; + width: 0%; + background: var(--accent); + transition: width 0.2s ease; +} + +.archive-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; +} +.archive-head .section-heading { + margin: 0; + border: none; + padding: 0; +} + +.icon-btn { + background: transparent; + border: 1px solid var(--rule); + padding: 5px 8px; + cursor: pointer; + color: var(--ink-soft); + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; +} +.icon-btn:hover { + border-color: var(--ink); + color: var(--accent); +} + +.pst-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; } + +.pst-item { + padding: 14px 14px; + border: 1px solid var(--rule); + background: var(--paper); + cursor: pointer; + transition: all 0.15s ease; + position: relative; +} +.pst-item:hover { + border-color: var(--ink); + transform: translateX(2px); +} +.pst-item.active { + border-color: var(--ink); + background: var(--ink); + color: var(--paper); + box-shadow: 4px 4px 0 var(--accent); +} +.pst-item.active .pst-meta { color: color-mix(in srgb, var(--paper) 65%, transparent); } + +.pst-name { + font-size: 1rem; + font-weight: 500; + letter-spacing: -0.01em; + margin: 0 0 6px; + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 8px; +} +.pst-name-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} +.pst-menu { + background: none; + border: none; + color: inherit; + opacity: 0.4; + cursor: pointer; + font-size: 1.1rem; + line-height: 1; + padding: 0 4px; + letter-spacing: 0.1em; + transition: opacity 0.15s, color 0.15s; +} +.pst-menu:hover { opacity: 1; color: var(--accent); } +.pst-item.active .pst-menu:hover { color: var(--accent-soft); } + +.pst-meta { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.08em; + color: var(--ink-muted); + display: flex; + justify-content: space-between; + gap: 8px; +} + +.pst-status { + text-transform: uppercase; + letter-spacing: 0.12em; +} +.pst-status.indexing { color: var(--warn); } +.pst-status.ready { color: var(--ok); } +.pst-status.failed { color: var(--accent); } +.pst-item.active .pst-status.indexing { color: color-mix(in srgb, var(--warn) 70%, var(--paper) 30%); } +.pst-item.active .pst-status.ready { color: color-mix(in srgb, var(--ok) 70%, var(--paper) 30%); } +.pst-item.active .pst-status.failed { color: var(--accent-soft); } + +.empty-state { + color: var(--ink-muted); + font-style: italic; + text-align: center; + padding: 20px 0; + font-size: 0.9rem; +} + +/* ============================================================ + FOLDER GROUPS IN SIDEBAR + ============================================================ */ +.archive-actions { + display: flex; + gap: 4px; +} + +.folder-group { + list-style: none; + padding: 0; + margin: 0; +} + +.folder-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 4px; + margin: 14px 0 6px; + cursor: pointer; + user-select: none; + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--ink-soft); + border-bottom: 1px solid var(--rule); + transition: color 0.15s; +} +.folder-group:first-child .folder-header { margin-top: 4px; } + +.folder-header:hover { color: var(--accent); } + +.folder-header-left { + display: flex; + align-items: center; + gap: 6px; + overflow: hidden; +} + +.folder-caret { + display: inline-block; + width: 8px; + transition: transform 0.2s; + color: var(--ink-muted); +} +.folder-group.collapsed .folder-caret { transform: rotate(-90deg); } + +.folder-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.folder-count { + color: var(--ink-muted); + font-size: 0.65rem; + margin-left: 4px; +} + +.folder-menu-btn { + background: none; + border: none; + color: var(--ink-muted); + cursor: pointer; + padding: 2px 6px; + font-size: 0.9rem; + line-height: 1; + opacity: 0; + transition: opacity 0.15s, color 0.15s; +} +.folder-header:hover .folder-menu-btn { opacity: 1; } +.folder-menu-btn:hover { color: var(--accent); } + +.folder-items { + display: flex; + flex-direction: column; + gap: 8px; + overflow: hidden; + transition: max-height 0.25s ease-out; + max-height: 2000px; +} +.folder-group.collapsed .folder-items { + max-height: 0; +} + +.folder-empty { + font-style: italic; + font-size: 0.8rem; + color: var(--ink-muted); + padding: 6px 4px; +} + +/* Context menu for folder/PST actions */ +.context-menu { + position: fixed; + background: var(--paper); + border: 1px solid var(--ink); + box-shadow: 4px 4px 0 var(--rule); + z-index: 500; + min-width: 160px; + padding: 4px 0; +} +.context-menu button { + display: block; + width: 100%; + text-align: left; + background: none; + border: none; + padding: 8px 14px; + font-family: var(--font-display); + font-size: 0.9rem; + color: var(--ink); + cursor: pointer; +} +.context-menu button:hover { + background: var(--paper-deep); + color: var(--accent); +} +.context-menu button.danger:hover { + color: var(--accent); +} +.context-menu hr { + border: none; + border-top: 1px solid var(--rule-soft); + margin: 4px 0; +} + +/* ============================================================ + MODAL + ============================================================ */ +.modal { + position: fixed; + inset: 0; + background: color-mix(in srgb, var(--ink) 40%, transparent); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + z-index: 200; + animation: fadeIn 0.2s ease-out; +} + +.modal-card { + background: var(--paper); + border: 1px solid var(--ink); + padding: 36px 36px 30px; + max-width: 420px; + width: 100%; + box-shadow: 8px 8px 0 var(--accent); + position: relative; +} +.modal-card h2 { + margin: 0 0 4px; + font-size: 1.5rem; + font-weight: 500; + letter-spacing: -0.015em; +} +.modal-subtitle { + margin: 0 0 24px; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--ink-muted); + letter-spacing: 0.06em; +} +.modal-close { + position: absolute; + top: 10px; + right: 14px; + background: none; + border: none; + font-size: 1.8rem; + line-height: 1; + cursor: pointer; + color: var(--ink-muted); + transition: color 0.15s, transform 0.15s; +} +.modal-close:hover { + color: var(--accent); + transform: rotate(90deg); +} + +.auth-label select { + font-family: var(--font-display); + font-size: 1rem; + padding: 10px 12px; + border: 1px solid var(--ink); + background: var(--paper-deep); + color: var(--ink); + outline: none; + cursor: pointer; +} +.auth-label select:focus { + background: var(--paper); + box-shadow: 3px 3px 0 var(--accent); +} + +/* ============================================================ + READER + ============================================================ */ +.reader { + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; +} + +.reader-head { + padding: 28px 40px 20px; + border-bottom: 1px solid var(--rule); + background: + linear-gradient(180deg, rgba(232, 225, 211, 0.3), transparent); +} + +.reader-title h2 { + margin: 0; + font-size: 2rem; + font-weight: 500; + letter-spacing: -0.02em; + line-height: 1.1; +} + +.reader-meta { + margin: 6px 0 20px; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.search-wrap { + display: grid; + grid-template-columns: 1fr 200px; + gap: 10px; +} + +#searchInput, #folderFilter { + font-family: var(--font-display); + font-size: 1rem; + padding: 10px 14px; + border: 1px solid var(--ink); + background: var(--paper); + color: var(--ink); + outline: none; + transition: box-shadow 0.15s; +} +#searchInput:focus, #folderFilter:focus { + box-shadow: 3px 3px 0 var(--accent); +} +#searchInput:disabled, #folderFilter:disabled { + border-color: var(--rule); + color: var(--ink-muted); + background: var(--paper-deep); + cursor: not-allowed; +} +#searchInput::placeholder { + color: var(--ink-muted); + font-style: italic; +} +#folderFilter { cursor: pointer; font-family: var(--font-mono); font-size: 0.8rem; } + +.reader-body { + flex: 1; + overflow-y: auto; + padding: 20px 40px; + position: relative; +} + +.reader-placeholder { + text-align: center; + padding: 80px 20px; + color: var(--ink-muted); +} +.placeholder-ornament { + font-size: 2rem; + color: var(--accent); + margin-bottom: 16px; +} +.reader-placeholder p { + font-style: italic; + font-size: 1.1rem; + margin: 0; +} + +/* ============================================================ + MESSAGE LIST + ============================================================ */ +.message-list { + display: flex; + flex-direction: column; +} + +.message-row { + padding: 16px 0; + border-bottom: 1px solid var(--rule-soft); + cursor: pointer; + transition: all 0.12s ease; + display: grid; + grid-template-columns: 180px 1fr 140px; + gap: 20px; + align-items: baseline; +} +.message-row:hover { + background: color-mix(in srgb, var(--paper-deep) 70%, transparent); + padding-left: 8px; + padding-right: 8px; +} + +.mr-sender { + font-weight: 500; + color: var(--ink); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.95rem; +} + +.mr-main { + min-width: 0; + overflow: hidden; +} +.mr-subject { + font-size: 1.05rem; + font-weight: 500; + margin: 0 0 4px; + color: var(--ink); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mr-snippet { + font-size: 0.85rem; + color: var(--ink-muted); + line-height: 1.5; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; +} +.mr-snippet mark { + background: var(--highlight); + color: var(--ink); + padding: 0 2px; + font-weight: 500; +} + +.mr-meta { + text-align: right; + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--ink-muted); + letter-spacing: 0.04em; +} +.mr-folder { + margin-top: 4px; + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--accent); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mr-attached { + display: inline-block; + margin-left: 4px; + color: var(--accent); +} + +/* ============================================================ + MESSAGE VIEW + ============================================================ */ +.message-view { + position: absolute; + inset: 20px 40px; + background: var(--paper); + border: 1px solid var(--ink); + box-shadow: 8px 8px 0 var(--rule); + overflow-y: auto; + padding: 40px 50px; + animation: slideIn 0.25s ease-out; +} + +@keyframes slideIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.close-btn { + position: absolute; + top: 14px; + right: 18px; + background: none; + border: none; + font-size: 2rem; + line-height: 1; + cursor: pointer; + color: var(--ink-muted); + transition: color 0.15s, transform 0.15s; +} +.close-btn:hover { + color: var(--accent); + transform: rotate(90deg); +} + +.message-header { + border-bottom: 2px solid var(--ink); + padding-bottom: 20px; + margin-bottom: 24px; +} + +.message-subject { + font-size: 1.75rem; + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.015em; + margin-bottom: 16px; +} + +.message-meta { + display: grid; + grid-template-columns: 80px 1fr; + gap: 4px 20px; + margin: 0; +} +.message-meta dt { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--ink-muted); + padding-top: 2px; +} +.message-meta dd { + margin: 0; + font-size: 0.95rem; + color: var(--ink-soft); + word-break: break-word; +} + +.message-body { + font-family: var(--font-display); + font-size: 1rem; + line-height: 1.7; + color: var(--ink); + white-space: pre-wrap; + word-wrap: break-word; + max-width: 70ch; +} + +/* ============================================================ + PAGINATOR + ============================================================ */ +.paginator { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 40px; + border-top: 1px solid var(--rule); + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--ink-soft); + background: var(--paper-deep); +} +.paginator button { + background: none; + border: 1px solid var(--ink); + padding: 6px 14px; + cursor: pointer; + font-family: inherit; + font-size: inherit; + color: var(--ink); + transition: all 0.15s; +} +.paginator button:hover:not(:disabled) { + background: var(--ink); + color: var(--paper); +} +.paginator button:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +/* ============================================================ + TOAST + ============================================================ */ +.toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: var(--ink); + color: var(--paper); + padding: 12px 20px; + font-family: var(--font-mono); + font-size: 0.8rem; + letter-spacing: 0.06em; + border-left: 3px solid var(--accent); + box-shadow: var(--shadow); + z-index: 1000; + animation: toastSlide 0.3s ease-out; +} +@keyframes toastSlide { + from { opacity: 0; transform: translate(-50%, 20px); } + to { opacity: 1; transform: translate(-50%, 0); } +} + +.hidden { display: none !important; } + +/* ============================================================ + AUTH SCREENS (login + setup) + ============================================================ */ +.auth-screen { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + background: + radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 45%), + radial-gradient(circle at 70% 80%, color-mix(in srgb, var(--ink) 10%, transparent), transparent 55%), + var(--paper); + z-index: 100; + animation: fadeIn 0.3s ease-out; +} +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.auth-card { + width: 100%; + max-width: 440px; + background: var(--paper); + border: 1px solid var(--ink); + padding: 44px 40px 36px; + box-shadow: 8px 8px 0 var(--rule); + position: relative; +} +.auth-card::before { + content: ""; + position: absolute; + top: -6px; + left: -6px; + right: 6px; + bottom: 6px; + border: 1px solid var(--ink); + pointer-events: none; + z-index: -1; + background: var(--paper-deep); +} + +.auth-brand { + text-align: center; + margin-bottom: 28px; + padding-bottom: 20px; + border-bottom: 1px solid var(--rule); +} +.auth-brand svg { color: var(--ink); margin: 0 auto 12px; display: block; } +.auth-brand h1 { + margin: 0; + font-size: 2rem; + font-weight: 500; + letter-spacing: -0.02em; + line-height: 1; +} +.auth-tagline { + margin: 8px 0 0; + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.auth-form { display: flex; flex-direction: column; gap: 16px; } + +.auth-intro { + margin: 0 0 6px; + font-size: 0.92rem; + color: var(--ink-soft); + line-height: 1.5; + font-style: italic; +} + +.auth-label { + display: flex; + flex-direction: column; + gap: 6px; +} +.auth-label > span { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--ink-soft); +} +.auth-label small { + font-family: var(--font-mono); + font-size: 0.66rem; + color: var(--ink-muted); + letter-spacing: 0.04em; +} +.auth-label input { + font-family: var(--font-display); + font-size: 1rem; + padding: 10px 12px; + border: 1px solid var(--ink); + background: var(--paper-deep); + color: var(--ink); + outline: none; + transition: box-shadow 0.15s, background 0.15s; +} +.auth-label input:focus { + background: var(--paper); + box-shadow: 3px 3px 0 var(--accent); +} + +.auth-btn { + margin-top: 8px; + padding: 12px 18px; + font-family: var(--font-display); + font-size: 1.05rem; + font-weight: 500; + letter-spacing: -0.01em; + background: var(--ink); + color: var(--paper); + border: 1px solid var(--ink); + cursor: pointer; + transition: all 0.15s; +} +.auth-btn:hover { + background: var(--accent); + border-color: var(--accent); + transform: translate(-1px, -1px); + box-shadow: 3px 3px 0 var(--ink); +} + +.auth-error { + padding: 10px 14px; + background: color-mix(in srgb, var(--accent) 10%, transparent); + border-left: 3px solid var(--accent); + color: var(--accent); + font-size: 0.88rem; + font-family: var(--font-mono); + letter-spacing: 0.02em; +} + +/* ============================================================ + USER CHIP in masthead + ============================================================ */ +.user-chip { + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-soft); + padding: 4px 10px; + border: 1px solid var(--rule); + background: var(--paper-deep); + margin-left: 12px; +} + +.logout-btn { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--ink-soft); + background: transparent; + border: 1px solid var(--rule); + padding: 5px 10px; + cursor: pointer; + transition: all 0.15s; + margin-left: 8px; +} +.logout-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* ============================================================ + THEME TOGGLE + ============================================================ */ +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid var(--rule); + background: var(--paper-deep); + color: var(--ink-soft); + cursor: pointer; + padding: 0; + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} +.theme-toggle:hover { + border-color: var(--accent); + color: var(--accent); + transform: translateY(-1px); +} +.theme-toggle svg { + width: 16px; + height: 16px; + transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} +.theme-toggle:hover svg { + transform: rotate(25deg); +} +:root[data-theme="dark"] .theme-toggle:hover svg { + transform: rotate(-25deg); +} + +/* Icon visibility — show sun in light mode, moon in dark mode */ +.theme-toggle .icon-sun { display: inline-block; } +.theme-toggle .icon-moon { display: none; } +:root[data-theme="dark"] .theme-toggle .icon-sun { display: none; } +:root[data-theme="dark"] .theme-toggle .icon-moon { display: inline-block; } + +/* Floating variant shown on auth/setup screens */ +.theme-toggle-float { + position: fixed; + top: 20px; + right: 20px; + z-index: 200; +} +/* Hide the floating toggle once the main app is visible (in-masthead toggle takes over) */ +body:has(#mainApp:not(.hidden)) .theme-toggle-float { display: none; } + +/* ============================================================ + SCROLLBARS + ============================================================ */ +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: var(--rule); + border: 2px solid var(--paper); +} +::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); } + +/* ============================================================ + RESPONSIVE + ============================================================ */ +@media (max-width: 900px) { + .workspace { grid-template-columns: 1fr; } + .sidebar { border-right: none; border-bottom: 1px solid var(--rule); max-height: 50vh; } + .masthead { padding: 20px 24px 14px; flex-wrap: wrap; gap: 16px; } + .reader-head, .reader-body, .paginator { padding-left: 24px; padding-right: 24px; } + .search-wrap { grid-template-columns: 1fr; } + .message-row { grid-template-columns: 1fr; gap: 6px; } + .mr-meta { text-align: left; } + .message-view { inset: 12px; padding: 24px 20px; } + .message-meta { grid-template-columns: 60px 1fr; } +} diff --git a/OLD_VERSIONS/v2.1 b/OLD_VERSIONS/v2.1 new file mode 160000 index 0000000..a95f71d --- /dev/null +++ b/OLD_VERSIONS/v2.1 @@ -0,0 +1 @@ +Subproject commit a95f71dc2a9a621e6bf24a525b27d6c6833d6b3a diff --git a/OLD_VERSIONS/v2.2/pst-indexer.zip b/OLD_VERSIONS/v2.2/pst-indexer.zip new file mode 100644 index 0000000000000000000000000000000000000000..674bbddb3cf014bfa9f710ad9bf71b917bb83f9f GIT binary patch literal 74484 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

    e(QZn}ZTs5I=SOYEPSwPk_NaGAVF-ullBmHW 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md new file mode 100644 index 0000000..842fd15 --- /dev/null +++ b/README.md @@ -0,0 +1,1172 @@ +# PST Archive — Mail Indexer + +A self-hosted, Dockerized application for **uploading Microsoft Outlook PST/OST files**, **indexing every email inside them**, and **searching the contents through a web browser** — with full **user authentication**, **multi-factor authentication (MFA)**, **encrypted-at-rest credentials**, and a **built-in administration section** for admin users. + +Designed for **forensic review, e-discovery, personal archive browsing, and migration audits** — any situation where you need to crack open a PST and actually find things inside it without loading it into Outlook. + +--- + +## What's new in v2.2 + +- **Administration now runs inside the main application** — there is no longer a + separate admin process or admin port (previously the panel ran on its own + port, e.g. mapped to `9001`). Everything is served on the single app port + (`8000` by default). +- **"Administration" button** — admin users see an **ADMINISTRATION** button in + the top-right of the masthead, next to their username. It opens the + administration section at `/admin`. The button is not shown to non-admin + users. +- **Access is restricted to admins** — the administration UI and every + `/api/admin/*` endpoint require an authenticated session **and** the `admin` + role. Non-admins receive `403`; unauthenticated requests receive `401`. +- **Footer** — every in-app page (the archive view and the administration view) + now shows `© 2026 Martinhal IT - Joao Vaz - Version 2.2`. The login/setup + screen has no footer. +- **Updating is drop-in** — extract this release over your existing install and + rebuild; see [Updating to a new version](#updating-to-a-new-version). Your + `./data` directory (database, uploaded PSTs, encryption key) is never touched. + +> Historical note: some deep-dive sections below still describe the original +> two-port design (main app + separate admin panel) for reference. Operationally, +> as of v2.2 both are the same process on one port, with administration at +> `/admin`. + +--- + +## Table of Contents + +1. [Features](#features) +2. [Quick Start](#quick-start) +3. [First-Run Setup](#first-run-setup) +4. [Running as a Daemon](#running-as-a-daemon) +5. [Theming & Dark Mode](#theming--dark-mode) +6. [Authentication & Security Model](#authentication--security-model) +7. [Admin Panel](#admin-panel) +8. [Organizing PSTs with Folders](#organizing-psts-with-folders) +9. [Folder Access Permissions](#folder-access-permissions) +10. [Multi-Factor Authentication (MFA)](#multi-factor-authentication-mfa) +11. [Encryption at Rest](#encryption-at-rest) +12. [How It Works](#how-it-works) +13. [Architecture](#architecture) +14. [Project Layout](#project-layout) +15. [Data Model](#data-model) +16. [REST API Reference](#rest-api-reference) +17. [Frontend Interfaces](#frontend-interfaces) +18. [Search Syntax](#search-syntax) +19. [Configuration](#configuration) +20. [Persistence & Backup](#persistence--backup) +21. [Troubleshooting](#troubleshooting) +22. [Performance Notes](#performance-notes) +23. [Security Considerations](#security-considerations) +24. [Extending the App](#extending-the-app) + +--- + +## Features + +### Core +- **Drag-and-drop upload** of `.pst` and `.ost` files through the browser +- **Streaming upload with progress bar** — handles multi-gigabyte archives +- **Background indexing** into SQLite with **FTS5 full-text search** (ranked, prefix-matched, with highlighted snippets) +- **Multi-archive support**: switch between uploaded PSTs; each is searched independently +- **Folder organization**: create folders in the sidebar to group related PST files (e.g. by client, case, year). Move archives between folders, rename folders, or delete folders (any archives inside are safely moved to Uncategorized) +- **Per-folder access permissions**: admins can restrict which non-admin users can view each folder. A single user can be granted access to many folders; a single folder can be shared with many users (true many-to-many) +- **Folder filter**: narrow search to any folder within the archive (Inbox, Sent Items, nested folders) +- **Full message viewer**: subject, sender, recipients, date, folder, attachments, body +- **Persistent storage**: everything lives in a mounted `./data` volume + +### Security +- **First-run setup wizard** forces creation of an initial administrator before any other action is possible +- **Password authentication** using **Argon2id** (winner of the Password Hashing Competition) with constant-time verification +- **TOTP-based MFA** (RFC 6238; compatible with Google Authenticator, Authy, 1Password, Microsoft Authenticator, and every other standard authenticator app) +- **Encryption at rest** for all user PII — usernames, password hashes, and TOTP secrets are stored as Fernet (AES-128-CBC + HMAC-SHA256) ciphertexts +- **Master key** auto-generated on first run and stored with `chmod 600`, or overridable via `MASTER_SECRET` environment variable +- **HMAC-based username lookup** — usernames are never stored in plaintext but are still indexable for login +- **JWT session cookies**, signed with an HKDF-derived sub-key, HttpOnly, SameSite=Lax +- **Rate limiting** on login attempts (10 failures per 5 minutes per IP+username) +- **Admin-only user management**: create, delete, role-change, password-reset, MFA-enroll other users +- **Admin file & folder management**: system-wide view of every uploaded PST and every folder, with create/rename/delete for folders and force-delete for any file regardless of uploader +- **Built-in administration section** for admins (at `/admin`) — restricted to the `admin` role, and lockable at the path level behind a reverse proxy/VPN + +### UI +- **Light and dark themes** on both the main app and the admin panel, with a sun/moon toggle in the masthead and on login/setup screens +- Preference persists in `localStorage` per-browser and respects the operating system's `prefers-color-scheme` on first visit +- Theme applied inline in `` before stylesheets load — **no flash of wrong theme** on page load +- Dark mode uses a custom "inked paper" palette (deep charcoal with warm paper-cream text and softened oxblood accent) — intentional character, not generic-dark +- Editorial typography (Fraunces serif + JetBrains Mono), grain overlay, responsive layout down to mobile widths + +--- + +## Quick Start + +### Prerequisites + +- Docker 20.10 or newer +- Docker Compose v2 +- A `.pst` or `.ost` file to index + +### Run (as a daemon — recommended) + +```bash +docker compose up -d --build +``` + +The `-d` flag runs the container in the background (detached). On the first run this takes **3–5 minutes** because `libpff-python` compiles a C extension from source; subsequent startups are nearly instant. + +The container is configured with `restart: unless-stopped`, so it will automatically come back up after a reboot or a crash, and keep running across Docker daemon restarts — until you explicitly stop it. + +Once the container is up: + +- **App**: `http://localhost:8000` +- **Administration**: `http://localhost:8000/admin` — or just click the + **ADMINISTRATION** button in the top-right after signing in as an admin. + (Non-admin users do not see the button and cannot reach `/admin`.) + +### Foreground mode (useful for debugging) + +```bash +docker compose up --build +``` + +This runs the container attached to your terminal so you can see logs inline. Press `Ctrl-C` to stop. + +### Tail the logs + +```bash +docker compose logs -f # follow both streams +docker compose logs -f pst-indexer # explicit service name +``` + +### Check status + +```bash +docker compose ps # is it running? +docker compose top # which processes? +``` + +### Stop + +```bash +docker compose stop # stop, keep data, keep container +docker compose down # stop and remove the container, keep data volume +``` + +### Full reset + +```bash +docker compose down +rm -rf ./data # erases everything including users and the master key +docker compose up -d --build +``` + +### Restart / update + +```bash +# After editing code or pulling new images: +docker compose up -d --build # rebuild & restart with one command +# Or just restart without rebuilding: +docker compose restart +``` + +--- + +## First-Run Setup + +On the very first visit to `http://localhost:8000`, you will be presented with a **setup wizard** that asks you to create the initial administrator: + +1. Choose a username (3–64 characters) +2. Choose a password (8+ characters — use a passphrase you'll remember) +3. Confirm the password + +The setup endpoint is only callable **while the users table is empty**. Once the first admin is created, the endpoint returns `409 Conflict` and is effectively disabled. + +After setup, you are automatically signed in and landed on the main app. + +**Heads up:** the very first account created through the setup wizard is an **administrator**. After that, sign in normally and use the **ADMINISTRATION** button (top-right) to manage users, folders, files, and permissions. The administration section and its API are only accessible to admin users. + +--- + +## Running as a Daemon + +By default, this app is designed to **run as a long-lived background service**. The `docker-compose.yml` sets `restart: unless-stopped`, which means once started, the container will: + +- Keep running after you close your terminal +- Automatically restart if the process crashes +- Automatically restart when the host reboots and Docker comes back up +- **Not** restart if you explicitly stop it with `docker compose stop` (hence "unless-stopped") + +### Starting the daemon + +```bash +docker compose up -d --build +``` + +The `-d` flag is short for "detached" — the container is launched in the background and the command returns immediately. Your terminal is free to do other things; the container keeps running. + +### Verifying it's running + +```bash +docker compose ps +``` + +You should see output like: + +``` +NAME IMAGE STATUS PORTS +pst-indexer pst-indexer-pst-indexer Up 2 minutes (healthy) 0.0.0.0:8000->8000/tcp +``` + +The `(healthy)` flag comes from the built-in healthcheck, which probes both `/api/health` endpoints every 30 seconds. + +### Watching logs + +Since the container is detached, you won't see logs in your terminal by default. Attach to them on demand: + +```bash +docker compose logs # print recent logs and exit +docker compose logs -f # follow (live tail), Ctrl-C to detach +docker compose logs -f --tail=100 # follow, starting from the last 100 lines +docker compose logs --since 1h # only logs from the last hour +``` + +Detaching from `-f` does **not** stop the container — it just stops showing you the logs. + +### Stopping + +```bash +docker compose stop # stop the container but keep it around +docker compose start # start it again if previously stopped +docker compose restart # stop and start in one command +docker compose down # stop and remove the container (data volume is kept) +docker compose down -v # stop, remove container, AND remove anonymous volumes +``` + +None of these delete your `./data` directory — that's a bind mount, not a Docker-managed volume, so it's always yours. + +### Survives reboots + +Because `restart: unless-stopped` is set, after a host reboot Docker will automatically start the container as soon as the Docker daemon comes up. Nothing manual required on your part. + +If you ever explicitly stopped it with `docker compose stop`, Docker will *not* auto-start it after reboot (that's the whole point of "unless-stopped"). Run `docker compose start` or `docker compose up -d` to bring it back. + +### Updating to a new version + +This release is a **drop-in update**: extract the archive over your existing +installation directory (the one containing `docker-compose.yml` and your +`data/` folder) and rebuild. The archive contains only application code — it +has **no `data/` directory**, so your database, uploaded PSTs, and encryption +key are never overwritten. + +```bash +# From the parent directory that holds your existing install folder: +# (the folder with docker-compose.yml and data/ inside it) +7z x pst-archive-v2.2.7z # extract, overwriting code files in place + +# Then rebuild and recreate the container: +cd +docker compose up -d --build +``` + +Docker Compose compares the current image to the built one; if they differ, it rebuilds and recreates the container with zero-downtime-style replacement. Your `./data` volume is preserved throughout. + +**Notes for this update (v2.2):** + +- The separate admin port is gone. If your `docker-compose.yml` previously + published a second port for the admin panel (for example `"9001:8001"` or + `"8001:8001"`), that line has been removed in the shipped compose file. If you + maintain a customized compose file, simply delete the admin-port line — the + administration section is now served on the main port at `/admin`. +- No database migration is required. The schema is unchanged, so all existing + users, folders, files, permissions, and MFA enrollments carry over as-is. +- A rebuild (`--build`) is required so the updated backend and frontend are + baked into the image. + +--- + +## Theming & Dark Mode + +Both the main app and the admin panel support **light and dark themes**, with a sun/moon toggle visible in the masthead (when signed in) and floating in the top-right corner on the login/setup screens. + +### How the toggle works + +- Click the icon to flip between themes +- Choice is saved to `localStorage["pst-theme"]` in your browser +- On a fresh browser (nothing saved), the app respects your operating system's `prefers-color-scheme` setting +- The theme is applied by an inline ` + + +

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/backend/admin_app.py b/backend/admin_app.py new file mode 100644 index 0000000..917217b --- /dev/null +++ b/backend/admin_app.py @@ -0,0 +1,597 @@ +""" +PST Indexer — Admin Panel (DEPRECATED / no longer launched). + +As of v2.2 the administration section runs inside the main application +(app.py), served at /admin and reachable via the "Administration" button that +is shown to admin users. The entrypoint no longer starts this module, and no +separate admin port is exposed. This file is retained only for reference and +for backwards compatibility with any custom tooling that imported it; it is +safe to ignore. See app.py for the live administration endpoints +(namespaced under /api/admin). + +Original description below. + +Separate FastAPI instance on its own port so operators can place it behind a +more restrictive firewall rule or VPN than the main app. + +Exposes: +- Admin login (separate session cookie, scoped "admin") +- List, create, delete users +- Toggle admin role +- Reset passwords +- Enroll / confirm / disable MFA for any user (admin-managed flow) +- Self-service MFA for the currently logged-in admin +""" +import io +import os +from pathlib import Path +from typing import Optional + +import qrcode +from fastapi import ( + Depends, FastAPI, HTTPException, Request, Response, status, +) +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import StreamingResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +import auth +from auth import ( + ADMIN_SESSION_COOKIE_NAME, SESSION_TTL_SECONDS, + User, begin_mfa_enrollment, confirm_mfa_enrollment, count_admins, + count_users, create_session_token, create_user, delete_user, + disable_mfa, find_user_by_id, find_user_by_username, + get_totp_secret, list_users, login_limiter, record_login, + require_admin_user, set_user_role, totp_provisioning_uri, + update_password, verify_password, verify_totp, +) + +ADMIN_FRONTEND_DIR = Path(os.environ.get("ADMIN_FRONTEND_DIR", "/app/admin_frontend")) +COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "false").lower() == "true" + +app = FastAPI(title="PST Indexer Admin Panel") +app.add_middleware( + CORSMiddleware, + allow_origins=[], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.on_event("startup") +def _startup(): + auth.init_auth_schema() + + +def _set_admin_cookie(response: Response, token: str): + response.set_cookie( + key=ADMIN_SESSION_COOKIE_NAME, + value=token, + max_age=SESSION_TTL_SECONDS, + httponly=True, + secure=COOKIE_SECURE, + samesite="lax", + path="/", + ) + + +def _clear_admin_cookie(response: Response): + response.delete_cookie(key=ADMIN_SESSION_COOKIE_NAME, path="/") + + +# --------------------------------------------------------------------------- +# Auth +# --------------------------------------------------------------------------- +class LoginPayload(BaseModel): + username: str + password: str + totp_code: Optional[str] = None + + +@app.get("/api/health") +def health(): + return {"status": "ok", "admins": count_admins(), "users": count_users()} + + +@app.get("/api/setup/status") +def setup_status(): + """True if no users exist. Admin UI redirects to main app to complete setup.""" + return {"needs_setup": count_users() == 0} + + +@app.post("/api/login") +def admin_login(payload: LoginPayload, request: Request, response: Response): + ip = request.client.host if request.client else "unknown" + rate_key = f"admin:{ip}:{payload.username.lower()}" + if not login_limiter.check(rate_key): + raise HTTPException(429, "Too many login attempts. Please wait a few minutes.") + + found = find_user_by_username(payload.username) + if not found: + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid credentials") + user, pw_hash = found + + if not verify_password(pw_hash, payload.password): + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid credentials") + + if not user.is_admin: + raise HTTPException(403, "Admin access required") + + if user.mfa_enabled: + if not payload.totp_code: + return JSONResponse( + status_code=401, + content={"detail": "MFA required", "mfa_required": True}, + ) + secret = get_totp_secret(user.id) + if not secret or not verify_totp(secret, payload.totp_code): + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid MFA code") + + login_limiter.reset(rate_key) + record_login(user.id) + token = create_session_token(user, scope="admin") + _set_admin_cookie(response, token) + return {"user": user.public_dict()} + + +@app.post("/api/logout") +def admin_logout(response: Response): + _clear_admin_cookie(response) + return {"ok": True} + + +@app.get("/api/me") +def me(user: User = Depends(require_admin_user)): + return {"user": user.public_dict()} + + +# --------------------------------------------------------------------------- +# User management +# --------------------------------------------------------------------------- +class CreateUserPayload(BaseModel): + username: str + password: str + role: str = "user" + + +class UpdatePasswordPayload(BaseModel): + password: str + + +class UpdateRolePayload(BaseModel): + role: str + + +@app.get("/api/users") +def api_list_users(_: User = Depends(require_admin_user)): + return [u.public_dict() for u in list_users()] + + +@app.post("/api/users") +def api_create_user( + payload: CreateUserPayload, + admin: User = Depends(require_admin_user), +): + try: + user = create_user(payload.username, payload.password, role=payload.role) + except ValueError as e: + raise HTTPException(400, str(e)) + return user.public_dict() + + +@app.delete("/api/users/{user_id}") +def api_delete_user(user_id: str, admin: User = Depends(require_admin_user)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + + # Prevent deleting yourself + if target.id == admin.id: + raise HTTPException(400, "You cannot delete your own account") + + # Prevent deleting the last admin + if target.is_admin and count_admins() <= 1: + raise HTTPException(400, "Cannot delete the last remaining admin") + + delete_user(user_id) + return {"deleted": user_id} + + +@app.put("/api/users/{user_id}/role") +def api_set_role( + user_id: str, + payload: UpdateRolePayload, + admin: User = Depends(require_admin_user), +): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + + if target.id == admin.id and payload.role != "admin": + raise HTTPException(400, "You cannot demote your own account") + + if (target.is_admin and payload.role != "admin" + and count_admins() <= 1): + raise HTTPException(400, "Cannot demote the last remaining admin") + + try: + set_user_role(user_id, payload.role) + except ValueError as e: + raise HTTPException(400, str(e)) + + return find_user_by_id(user_id).public_dict() + + +@app.put("/api/users/{user_id}/password") +def api_update_password( + user_id: str, + payload: UpdatePasswordPayload, + admin: User = Depends(require_admin_user), +): + if not find_user_by_id(user_id): + raise HTTPException(404, "User not found") + try: + update_password(user_id, payload.password) + except ValueError as e: + raise HTTPException(400, str(e)) + return {"ok": True} + + +# --------------------------------------------------------------------------- +# MFA management +# --------------------------------------------------------------------------- +class ConfirmMfaPayload(BaseModel): + totp_code: str + + +@app.post("/api/users/{user_id}/mfa/begin") +def api_begin_mfa(user_id: str, admin: User = Depends(require_admin_user)): + """Provision a new TOTP secret for the target user (admin action). + Returns the secret + otpauth URI so the admin can show it to the user.""" + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + secret, uri = begin_mfa_enrollment(user_id) + return { + "secret": secret, + "otpauth_uri": uri, + "username": target.username, + "qr_url": f"/api/users/{user_id}/mfa/qr", + } + + +@app.get("/api/users/{user_id}/mfa/qr") +def api_mfa_qr(user_id: str, admin: User = Depends(require_admin_user)): + """Render a PNG QR code of the target user's current (unconfirmed) TOTP URI.""" + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + secret = get_totp_secret(user_id) + if not secret: + raise HTTPException(400, "No MFA enrollment in progress") + uri = totp_provisioning_uri(secret, target.username) + + qr = qrcode.QRCode(box_size=6, border=2) + qr.add_data(uri) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + buf = io.BytesIO() + img.save(buf, format="PNG") + buf.seek(0) + return StreamingResponse(buf, media_type="image/png") + + +@app.post("/api/users/{user_id}/mfa/confirm") +def api_confirm_mfa( + user_id: str, + payload: ConfirmMfaPayload, + admin: User = Depends(require_admin_user), +): + if not find_user_by_id(user_id): + raise HTTPException(404, "User not found") + if not confirm_mfa_enrollment(user_id, payload.totp_code): + raise HTTPException(400, "Invalid MFA code — enrollment not confirmed") + return find_user_by_id(user_id).public_dict() + + +@app.post("/api/users/{user_id}/mfa/disable") +def api_disable_mfa(user_id: str, admin: User = Depends(require_admin_user)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + disable_mfa(user_id) + return find_user_by_id(user_id).public_dict() + + +# --------------------------------------------------------------------------- +# Folder management (admin) +# --------------------------------------------------------------------------- +import uuid as _uuid +from datetime import datetime, timezone +from pathlib import Path as _Path +from auth import get_db + + +UPLOAD_DIR = _Path(os.environ.get("DATA_DIR", "/app/data")) / "uploads" + + +class AdminFolderPayload(BaseModel): + name: str + + +def _validate_name(name: str) -> str: + name = (name or "").strip() + if not name: + raise HTTPException(400, "Folder name cannot be empty") + if len(name) > 80: + raise HTTPException(400, "Folder name too long (max 80 characters)") + return name + + +@app.get("/api/folders") +def admin_list_folders(_: User = Depends(require_admin_user)): + with get_db() as conn: + rows = conn.execute( + """SELECT f.id, f.name, f.created_at, f.created_by, + COUNT(p.id) AS file_count + FROM pst_folders f + LEFT JOIN pst_files p ON p.folder_id = f.id + GROUP BY f.id + ORDER BY f.name COLLATE NOCASE""" + ).fetchall() + return [dict(r) for r in rows] + + +@app.post("/api/folders") +def admin_create_folder(payload: AdminFolderPayload, + admin: User = Depends(require_admin_user)): + name = _validate_name(payload.name) + with get_db() as conn: + existing = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) + ).fetchone() + if existing: + raise HTTPException(409, "A folder with that name already exists") + folder_id = _uuid.uuid4().hex + conn.execute( + """INSERT INTO pst_folders (id, name, created_at, created_by) + VALUES (?, ?, ?, ?)""", + (folder_id, name, datetime.now(timezone.utc).isoformat(), admin.id), + ) + conn.commit() + return {"id": folder_id, "name": name, "file_count": 0} + + +@app.put("/api/folders/{folder_id}") +def admin_rename_folder(folder_id: str, payload: AdminFolderPayload, + admin: User = Depends(require_admin_user)): + name = _validate_name(payload.name) + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + clash = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", + (name, folder_id), + ).fetchone() + if clash: + raise HTTPException(409, "A folder with that name already exists") + conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", + (name, folder_id)) + conn.commit() + return {"id": folder_id, "name": name} + + +@app.delete("/api/folders/{folder_id}") +def admin_delete_folder(folder_id: str, + admin: User = Depends(require_admin_user)): + """Delete a folder. PSTs inside it become uncategorized (folder_id = NULL).""" + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", + (folder_id,)) + conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) + conn.commit() + return {"deleted": folder_id} + + +# --------------------------------------------------------------------------- +# PST file management (admin) +# --------------------------------------------------------------------------- +@app.get("/api/pst-files") +def admin_list_pst_files(_: User = Depends(require_admin_user)): + with get_db() as conn: + rows = conn.execute( + """SELECT p.id, p.original_name, p.uploaded_at, p.size_bytes, + p.status, p.message_count, p.error, + p.folder_id, f.name AS folder_name, + p.uploaded_by + FROM pst_files p + LEFT JOIN pst_folders f ON f.id = p.folder_id + ORDER BY p.uploaded_at DESC""" + ).fetchall() + return [dict(r) for r in rows] + + +@app.delete("/api/pst-files/{pst_id}") +def admin_delete_pst(pst_id: str, admin: User = Depends(require_admin_user)): + with get_db() as conn: + row = conn.execute( + "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) + ).fetchone() + if not row: + raise HTTPException(404, "PST not found") + conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) + conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) + conn.commit() + try: + (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) + except Exception: + pass + return {"deleted": pst_id} + + +class AdminAssignPayload(BaseModel): + folder_id: Optional[str] = None + + +@app.put("/api/pst-files/{pst_id}/folder") +def admin_assign_pst_to_folder(pst_id: str, payload: AdminAssignPayload, + admin: User = Depends(require_admin_user)): + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_files WHERE id = ?", + (pst_id,)).fetchone(): + raise HTTPException(404, "PST not found") + if payload.folder_id: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (payload.folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", + (payload.folder_id, pst_id)) + conn.commit() + return {"pst_id": pst_id, "folder_id": payload.folder_id} + + +# --------------------------------------------------------------------------- +# Folder permissions (admin-only) +# Controls which user can access which folder. +# Admins ALWAYS have access to every folder regardless of this table — +# these grants are for non-admin users. +# --------------------------------------------------------------------------- +class PermissionToggle(BaseModel): + granted: bool + + +@app.get("/api/permissions") +def admin_list_permissions(_: User = Depends(require_admin_user)): + """Return the full matrix: users, folders, and all grants.""" + with get_db() as conn: + users = conn.execute("SELECT * FROM users ORDER BY created_at ASC").fetchall() + folders = conn.execute( + "SELECT id, name FROM pst_folders ORDER BY name COLLATE NOCASE" + ).fetchall() + grants = conn.execute( + "SELECT user_id, folder_id, granted_at FROM folder_permissions" + ).fetchall() + + from auth import _row_to_user + user_list = [] + for u in users: + pub = _row_to_user(u).public_dict() + user_list.append(pub) + + return { + "users": user_list, + "folders": [dict(f) for f in folders], + "grants": [dict(g) for g in grants], + } + + +@app.get("/api/users/{user_id}/folders") +def admin_user_folders(user_id: str, _: User = Depends(require_admin_user)): + """Folders a given user has been granted.""" + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + with get_db() as conn: + rows = conn.execute( + """SELECT f.id, f.name, p.granted_at + FROM folder_permissions p + JOIN pst_folders f ON f.id = p.folder_id + WHERE p.user_id = ? + ORDER BY f.name COLLATE NOCASE""", + (user_id,), + ).fetchall() + return { + "user": target.public_dict(), + "folders": [dict(r) for r in rows], + } + + +@app.get("/api/folders/{folder_id}/users") +def admin_folder_users(folder_id: str, _: User = Depends(require_admin_user)): + """Users granted on a given folder.""" + with get_db() as conn: + folder = conn.execute( + "SELECT id, name FROM pst_folders WHERE id = ?", (folder_id,) + ).fetchone() + if not folder: + raise HTTPException(404, "Folder not found") + + grant_rows = conn.execute( + """SELECT user_id, granted_at + FROM folder_permissions WHERE folder_id = ?""", + (folder_id,), + ).fetchall() + + # Hydrate user details (usernames are encrypted — need decrypt via find_user_by_id) + users_out = [] + for r in grant_rows: + u = find_user_by_id(r["user_id"]) + if u: + users_out.append({ + **u.public_dict(), + "granted_at": r["granted_at"], + }) + users_out.sort(key=lambda x: x["username"].lower()) + return { + "folder": dict(folder), + "users": users_out, + } + + +@app.put("/api/permissions/{user_id}/{folder_id}") +def admin_toggle_permission( + user_id: str, + folder_id: str, + payload: PermissionToggle, + admin: User = Depends(require_admin_user), +): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + + if target.is_admin: + # Admins implicitly have access to everything; refuse explicit grants + # so the UI doesn't confuse admins with checked-but-meaningless rows. + raise HTTPException( + 400, + "Admins have access to all folders automatically; permissions " + "only apply to non-admin users.", + ) + + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + + if payload.granted: + conn.execute( + """INSERT OR IGNORE INTO folder_permissions + (user_id, folder_id, granted_at, granted_by) + VALUES (?, ?, ?, ?)""", + (user_id, folder_id, + datetime.now(timezone.utc).isoformat(), admin.id), + ) + else: + conn.execute( + "DELETE FROM folder_permissions WHERE user_id = ? AND folder_id = ?", + (user_id, folder_id), + ) + conn.commit() + + return {"user_id": user_id, "folder_id": folder_id, "granted": payload.granted} + + +# --------------------------------------------------------------------------- +# Static admin frontend +# --------------------------------------------------------------------------- +if ADMIN_FRONTEND_DIR.exists(): + app.mount("/", StaticFiles(directory=str(ADMIN_FRONTEND_DIR), html=True), name="admin") diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..10cbd89 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,1234 @@ +""" +PST Indexer — Main application (port 8000). + +Serves: +- The web UI (login, setup wizard, archive browser) +- REST API for PST upload, indexing, search +- All data-access endpoints require an authenticated session + +First-run behavior: +- If no users exist, the UI routes users to a setup wizard. +- /api/setup is only callable while the users table is empty; after the first + admin is created, the endpoint returns 409. +""" +import io +import os +import re +import sqlite3 +import uuid +import asyncio +from datetime import datetime, timezone +from pathlib import Path +from typing import Optional + +import aiofiles +import pypff +import qrcode +from fastapi import ( + BackgroundTasks, Depends, FastAPI, File, HTTPException, + Request, Response, UploadFile, status, +) +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, FileResponse, StreamingResponse +from fastapi.staticfiles import StaticFiles +from pydantic import BaseModel + +import auth +from auth import ( + SESSION_COOKIE_NAME, SESSION_TTL_SECONDS, + User, count_users, count_admins, create_user, find_user_by_username, + find_user_by_id, get_totp_secret, verify_password, verify_totp, + record_login, create_session_token, login_limiter, require_app_user, + require_app_admin, get_db, + # Administration (merged in from the former standalone admin panel) + begin_mfa_enrollment, confirm_mfa_enrollment, delete_user, disable_mfa, + list_users, set_user_role, totp_provisioning_uri, update_password, +) + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- +DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) +UPLOAD_DIR = DATA_DIR / "uploads" +FRONTEND_DIR = Path(os.environ.get("FRONTEND_DIR", "/app/frontend")) +ADMIN_FRONTEND_DIR = Path(os.environ.get("ADMIN_FRONTEND_DIR", "/app/admin_frontend")) + +UPLOAD_DIR.mkdir(parents=True, exist_ok=True) +DATA_DIR.mkdir(parents=True, exist_ok=True) + +# Cookie security. Flip to True when serving over HTTPS. +COOKIE_SECURE = os.environ.get("COOKIE_SECURE", "false").lower() == "true" + + +# --------------------------------------------------------------------------- +# App DB schema (messages + pst_files) +# --------------------------------------------------------------------------- +def init_app_schema(): + with get_db() as conn: + conn.executescript(""" + CREATE TABLE IF NOT EXISTS pst_folders ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at TEXT NOT NULL, + created_by TEXT + ); + + CREATE TABLE IF NOT EXISTS folder_permissions ( + user_id TEXT NOT NULL, + folder_id TEXT NOT NULL, + granted_at TEXT NOT NULL, + granted_by TEXT, + PRIMARY KEY (user_id, folder_id) + ); + + CREATE INDEX IF NOT EXISTS idx_folder_perms_user ON folder_permissions(user_id); + CREATE INDEX IF NOT EXISTS idx_folder_perms_folder ON folder_permissions(folder_id); + + CREATE TABLE IF NOT EXISTS pst_files ( + id TEXT PRIMARY KEY, + filename TEXT NOT NULL, + original_name TEXT NOT NULL, + uploaded_at TEXT NOT NULL, + size_bytes INTEGER NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + message_count INTEGER DEFAULT 0, + error TEXT, + uploaded_by TEXT, + folder_id TEXT + ); + + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pst_id TEXT NOT NULL, + folder TEXT, + subject TEXT, + sender TEXT, + recipients TEXT, + sent_date TEXT, + body TEXT, + attachments TEXT, + FOREIGN KEY (pst_id) REFERENCES pst_files(id) ON DELETE CASCADE + ); + + CREATE INDEX IF NOT EXISTS idx_messages_pst ON messages(pst_id); + CREATE INDEX IF NOT EXISTS idx_messages_sent_date ON messages(sent_date); + CREATE INDEX IF NOT EXISTS idx_pst_files_folder ON pst_files(folder_id); + + CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( + subject, sender, recipients, body, folder, + content='messages', + content_rowid='id', + tokenize='porter unicode61' + ); + + CREATE TRIGGER IF NOT EXISTS messages_ai AFTER INSERT ON messages BEGIN + INSERT INTO messages_fts(rowid, subject, sender, recipients, body, folder) + VALUES (new.id, new.subject, new.sender, new.recipients, new.body, new.folder); + END; + + CREATE TRIGGER IF NOT EXISTS messages_ad AFTER DELETE ON messages BEGIN + INSERT INTO messages_fts(messages_fts, rowid, subject, sender, recipients, body, folder) + VALUES ('delete', old.id, old.subject, old.sender, old.recipients, old.body, old.folder); + END; + """) + # Backfill: add folder_id column to DBs created before this feature existed. + cols = [r["name"] for r in conn.execute("PRAGMA table_info(pst_files)").fetchall()] + if "folder_id" not in cols: + conn.execute("ALTER TABLE pst_files ADD COLUMN folder_id TEXT") + conn.execute("CREATE INDEX IF NOT EXISTS idx_pst_files_folder ON pst_files(folder_id)") + conn.commit() + + +# --------------------------------------------------------------------------- +# Access control helpers — which folders / PSTs is a given user allowed to see? +# +# Rules: +# - Admins see everything. +# - Regular users see only folders they have been explicitly granted, +# plus the implicit "Uncategorized" bucket (folder_id = NULL) — this is +# deliberately shared because otherwise newly-uploaded PSTs would be +# invisible until someone moved them into a permitted folder. +# - A PST is visible iff its folder is visible (or the PST is uncategorized). +# --------------------------------------------------------------------------- +def allowed_folder_ids_for(user) -> set[str]: + """Return the set of folder_ids a user is explicitly permitted on. + For admins this is not used — they see everything unconditionally.""" + with get_db() as conn: + rows = conn.execute( + "SELECT folder_id FROM folder_permissions WHERE user_id = ?", + (user.id,), + ).fetchall() + return {r["folder_id"] for r in rows} + + +def user_can_see_folder(user, folder_id: Optional[str]) -> bool: + """Can this user see content inside this folder? folder_id may be None + (Uncategorized), which is visible to everyone.""" + if user.is_admin: + return True + if folder_id is None: + return True # Uncategorized is shared + return folder_id in allowed_folder_ids_for(user) + + +def user_can_see_pst(user, pst_id: str) -> bool: + if user.is_admin: + return True + with get_db() as conn: + row = conn.execute( + "SELECT folder_id FROM pst_files WHERE id = ?", (pst_id,) + ).fetchone() + if not row: + return False + return user_can_see_folder(user, row["folder_id"]) + + +# --------------------------------------------------------------------------- +# PST indexing helpers (unchanged from pre-auth version) +# --------------------------------------------------------------------------- +def _safe_str(value) -> str: + if value is None: + return "" + if isinstance(value, bytes): + try: + return value.decode("utf-8", errors="replace") + except Exception: + return value.decode("latin-1", errors="replace") + return str(value) + + +def _extract_recipients(message) -> str: + parts = [] + try: + headers = _safe_str(getattr(message, "transport_headers", "")) + if headers: + for line in headers.split("\n"): + low = line.lower() + if low.startswith("to:") or low.startswith("cc:") or low.startswith("bcc:"): + parts.append(line.strip()) + except Exception: + pass + return " | ".join(parts) if parts else "" + + +def _extract_body(message) -> str: + for attr in ("plain_text_body", "html_body", "rtf_body"): + try: + body = getattr(message, attr, None) + if body: + text = _safe_str(body) + if attr == "html_body": + text = re.sub(r"<[^>]+>", " ", text) + text = re.sub(r"\s+", " ", text).strip() + if text: + return text + except Exception: + continue + return "" + + +def _extract_sender(message) -> str: + for attr in ("sender_name", "sender_email_address"): + try: + v = _safe_str(getattr(message, attr, "")) + if v: + return v + except Exception: + continue + try: + headers = _safe_str(getattr(message, "transport_headers", "")) + for line in headers.split("\n"): + if line.lower().startswith("from:"): + return line.split(":", 1)[1].strip() + except Exception: + pass + return "" + + +def _extract_date(message) -> Optional[str]: + try: + d = getattr(message, "delivery_time", None) or getattr(message, "client_submit_time", None) + if d: + if isinstance(d, datetime): + return d.isoformat() + return _safe_str(d) + except Exception: + pass + return None + + +def _extract_attachments(message) -> str: + names = [] + try: + count = getattr(message, "number_of_attachments", 0) + for i in range(count): + try: + att = message.get_attachment(i) + name = _safe_str(getattr(att, "name", "") or getattr(att, "long_filename", "")) + if name: + names.append(name) + except Exception: + continue + except Exception: + pass + return " | ".join(names) + + +def _walk_folders(folder, path=""): + try: + name = _safe_str(getattr(folder, "name", "") or "") + except Exception: + name = "" + current_path = f"{path}/{name}" if name else (path or "/") + try: + for i in range(folder.number_of_sub_messages): + try: + msg = folder.get_sub_message(i) + yield current_path, msg + except Exception: + continue + except Exception: + pass + try: + for i in range(folder.number_of_sub_folders): + try: + sub = folder.get_sub_folder(i) + yield from _walk_folders(sub, current_path) + except Exception: + continue + except Exception: + pass + + +def index_pst_file(pst_id: str, file_path: Path): + pff = None + try: + pff = pypff.file() + pff.open(str(file_path)) + root = pff.get_root_folder() + count = 0 + batch = [] + BATCH_SIZE = 500 + + with get_db() as conn: + for folder_path, msg in _walk_folders(root): + try: + batch.append(( + pst_id, folder_path, + _safe_str(getattr(msg, "subject", "")), + _extract_sender(msg), + _extract_recipients(msg), + _extract_date(msg), + _extract_body(msg), + _extract_attachments(msg), + )) + count += 1 + if len(batch) >= BATCH_SIZE: + conn.executemany( + """INSERT INTO messages + (pst_id, folder, subject, sender, recipients, sent_date, body, attachments) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + batch, + ) + conn.commit() + batch.clear() + except Exception as e: + print(f"[index] skip: {e}") + continue + + if batch: + conn.executemany( + """INSERT INTO messages + (pst_id, folder, subject, sender, recipients, sent_date, body, attachments) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + batch, + ) + conn.commit() + + conn.execute( + "UPDATE pst_files SET status = ?, message_count = ? WHERE id = ?", + ("ready", count, pst_id), + ) + conn.commit() + + print(f"[index] {pst_id}: {count} messages") + except Exception as e: + print(f"[index] failed: {e}") + with get_db() as conn: + conn.execute( + "UPDATE pst_files SET status = ?, error = ? WHERE id = ?", + ("failed", str(e), pst_id), + ) + conn.commit() + finally: + if pff is not None: + try: + pff.close() + except Exception: + pass + + +# --------------------------------------------------------------------------- +# FastAPI +# --------------------------------------------------------------------------- +app = FastAPI(title="PST Indexer") + +app.add_middleware( + CORSMiddleware, + allow_origins=[], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.on_event("startup") +def _startup(): + auth.init_auth_schema() + init_app_schema() + + +def _set_session_cookie(response: Response, token: str): + response.set_cookie( + key=SESSION_COOKIE_NAME, + value=token, + max_age=SESSION_TTL_SECONDS, + httponly=True, + secure=COOKIE_SECURE, + samesite="lax", + path="/", + ) + + +def _clear_session_cookie(response: Response): + response.delete_cookie(key=SESSION_COOKIE_NAME, path="/") + + +# ----- Setup / auth endpoints ----- + +class SetupRequest(BaseModel): + username: str + password: str + + +class LoginRequest(BaseModel): + username: str + password: str + totp_code: Optional[str] = None + + +@app.get("/api/setup/status") +def setup_status(): + """Lets the frontend decide whether to show the setup wizard.""" + return {"needs_setup": count_users() == 0} + + +@app.post("/api/setup") +def setup_admin(payload: SetupRequest, response: Response): + if count_users() > 0: + raise HTTPException(status.HTTP_409_CONFLICT, "Setup has already been completed") + + try: + user = create_user(payload.username, payload.password, role="admin") + except ValueError as e: + raise HTTPException(400, str(e)) + + record_login(user.id) + token = create_session_token(user, scope="app") + _set_session_cookie(response, token) + return {"user": user.public_dict()} + + +@app.post("/api/login") +def login(payload: LoginRequest, request: Request, response: Response): + ip = request.client.host if request.client else "unknown" + rate_key = f"{ip}:{payload.username.lower()}" + if not login_limiter.check(rate_key): + raise HTTPException(429, "Too many login attempts. Please wait a few minutes.") + + found = find_user_by_username(payload.username) + if not found: + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid credentials") + + user, stored_pw_hash = found + if not verify_password(stored_pw_hash, payload.password): + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid credentials") + + if user.mfa_enabled: + if not payload.totp_code: + # Tell the client MFA is required + return JSONResponse( + status_code=401, + content={"detail": "MFA required", "mfa_required": True}, + ) + secret = get_totp_secret(user.id) + if not secret or not verify_totp(secret, payload.totp_code): + login_limiter.record(rate_key) + raise HTTPException(401, "Invalid MFA code") + + login_limiter.reset(rate_key) + record_login(user.id) + token = create_session_token(user, scope="app") + _set_session_cookie(response, token) + return {"user": user.public_dict()} + + +@app.post("/api/logout") +def logout(response: Response): + _clear_session_cookie(response) + return {"ok": True} + + +@app.get("/api/me") +def me(user: User = Depends(require_app_user)): + return {"user": user.public_dict()} + + +# ----- PST endpoints (all require auth) ----- + +@app.get("/api/health") +def health(): + return {"status": "ok"} + + +@app.get("/api/pst-files") +def list_pst_files(user: User = Depends(require_app_user)): + with get_db() as conn: + if user.is_admin: + rows = conn.execute( + """SELECT id, original_name, uploaded_at, size_bytes, status, + message_count, error, folder_id + FROM pst_files ORDER BY uploaded_at DESC""" + ).fetchall() + else: + allowed = allowed_folder_ids_for(user) + if allowed: + placeholders = ",".join("?" for _ in allowed) + rows = conn.execute( + f"""SELECT id, original_name, uploaded_at, size_bytes, status, + message_count, error, folder_id + FROM pst_files + WHERE folder_id IS NULL OR folder_id IN ({placeholders}) + ORDER BY uploaded_at DESC""", + list(allowed), + ).fetchall() + else: + rows = conn.execute( + """SELECT id, original_name, uploaded_at, size_bytes, status, + message_count, error, folder_id + FROM pst_files WHERE folder_id IS NULL + ORDER BY uploaded_at DESC""" + ).fetchall() + return [dict(r) for r in rows] + + +# ----- Folder endpoints (shared with admin panel) ----- + +class FolderCreate(BaseModel): + name: str + + +class FolderRename(BaseModel): + name: str + + +class FolderAssign(BaseModel): + folder_id: Optional[str] = None # null = move to uncategorized + + +def _validate_folder_name(name: str) -> str: + name = (name or "").strip() + if not name: + raise HTTPException(400, "Folder name cannot be empty") + if len(name) > 80: + raise HTTPException(400, "Folder name too long (max 80 characters)") + return name + + +@app.get("/api/folders") +def list_folders(user: User = Depends(require_app_user)): + with get_db() as conn: + if user.is_admin: + rows = conn.execute( + """SELECT f.id, f.name, f.created_at, f.created_by, + COUNT(p.id) AS file_count + FROM pst_folders f + LEFT JOIN pst_files p ON p.folder_id = f.id + GROUP BY f.id + ORDER BY f.name COLLATE NOCASE""" + ).fetchall() + else: + allowed = allowed_folder_ids_for(user) + if not allowed: + return [] + placeholders = ",".join("?" for _ in allowed) + rows = conn.execute( + f"""SELECT f.id, f.name, f.created_at, f.created_by, + COUNT(p.id) AS file_count + FROM pst_folders f + LEFT JOIN pst_files p ON p.folder_id = f.id + WHERE f.id IN ({placeholders}) + GROUP BY f.id + ORDER BY f.name COLLATE NOCASE""", + list(allowed), + ).fetchall() + return [dict(r) for r in rows] + + +@app.post("/api/folders") +def create_folder(payload: FolderCreate, user: User = Depends(require_app_user)): + # Only admins can create folders — the admin panel is the authoritative + # management surface. (We still allow creation from the main app sidebar, + # but only for admin users.) + if not user.is_admin: + raise HTTPException(403, "Only administrators can create folders") + name = _validate_folder_name(payload.name) + with get_db() as conn: + existing = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) + ).fetchone() + if existing: + raise HTTPException(409, "A folder with that name already exists") + folder_id = uuid.uuid4().hex + conn.execute( + """INSERT INTO pst_folders (id, name, created_at, created_by) + VALUES (?, ?, ?, ?)""", + (folder_id, name, datetime.now(timezone.utc).isoformat(), user.id), + ) + conn.commit() + return {"id": folder_id, "name": name, "file_count": 0} + + +@app.put("/api/folders/{folder_id}") +def rename_folder(folder_id: str, payload: FolderRename, + user: User = Depends(require_app_user)): + if not user.is_admin: + raise HTTPException(403, "Only administrators can rename folders") + name = _validate_folder_name(payload.name) + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + clash = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", + (name, folder_id), + ).fetchone() + if clash: + raise HTTPException(409, "A folder with that name already exists") + conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", + (name, folder_id)) + conn.commit() + return {"id": folder_id, "name": name} + + +@app.delete("/api/folders/{folder_id}") +def delete_folder(folder_id: str, user: User = Depends(require_app_user)): + """Delete a folder. Any PSTs inside it become uncategorized (folder_id = NULL).""" + if not user.is_admin: + raise HTTPException(403, "Only administrators can delete folders") + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", + (folder_id,)) + conn.execute("DELETE FROM folder_permissions WHERE folder_id = ?", + (folder_id,)) + conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) + conn.commit() + return {"deleted": folder_id} + + +@app.put("/api/pst-files/{pst_id}/folder") +def assign_pst_to_folder(pst_id: str, payload: FolderAssign, + user: User = Depends(require_app_user)): + # Check the user can see the PST's current location AND the destination + if not user_can_see_pst(user, pst_id): + raise HTTPException(404, "PST not found") + if payload.folder_id and not user_can_see_folder(user, payload.folder_id): + raise HTTPException(403, "You do not have access to the target folder") + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_files WHERE id = ?", + (pst_id,)).fetchone(): + raise HTTPException(404, "PST not found") + if payload.folder_id: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (payload.folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", + (payload.folder_id, pst_id)) + conn.commit() + return {"pst_id": pst_id, "folder_id": payload.folder_id} + + +@app.post("/api/pst-files") +async def upload_pst( + background_tasks: BackgroundTasks, + file: UploadFile = File(...), + folder_id: Optional[str] = None, + user: User = Depends(require_app_user), +): + if not file.filename: + raise HTTPException(400, "No filename provided") + if not file.filename.lower().endswith((".pst", ".ost")): + raise HTTPException(400, "Only .pst and .ost files are supported") + + # Validate folder if provided (and verify user has access to it) + if folder_id: + with get_db() as conn: + exists = conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone() + if not exists: + raise HTTPException(400, "Target folder does not exist") + if not user_can_see_folder(user, folder_id): + raise HTTPException(403, "You do not have access to the target folder") + + pst_id = uuid.uuid4().hex + stored_name = f"{pst_id}_{file.filename}" + stored_path = UPLOAD_DIR / stored_name + + size = 0 + async with aiofiles.open(stored_path, "wb") as out: + while chunk := await file.read(1024 * 1024): + await out.write(chunk) + size += len(chunk) + + with get_db() as conn: + conn.execute( + """INSERT INTO pst_files + (id, filename, original_name, uploaded_at, size_bytes, status, + uploaded_by, folder_id) + VALUES (?, ?, ?, ?, ?, 'indexing', ?, ?)""", + (pst_id, stored_name, file.filename, + datetime.now(timezone.utc).isoformat(), size, user.id, folder_id), + ) + conn.commit() + + background_tasks.add_task(asyncio.to_thread, index_pst_file, pst_id, stored_path) + return {"id": pst_id, "original_name": file.filename, + "status": "indexing", "size_bytes": size, "folder_id": folder_id} + + +@app.delete("/api/pst-files/{pst_id}") +def delete_pst(pst_id: str, user: User = Depends(require_app_user)): + if not user_can_see_pst(user, pst_id): + raise HTTPException(404, "PST not found") + with get_db() as conn: + row = conn.execute( + "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) + ).fetchone() + if not row: + raise HTTPException(404, "PST not found") + conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) + conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) + conn.commit() + try: + (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) + except Exception: + pass + return {"deleted": pst_id} + + +@app.get("/api/pst-files/{pst_id}/folders") +def get_folders(pst_id: str, user: User = Depends(require_app_user)): + if not user_can_see_pst(user, pst_id): + raise HTTPException(404, "PST not found") + with get_db() as conn: + rows = conn.execute( + """SELECT folder, COUNT(*) as count FROM messages + WHERE pst_id = ? GROUP BY folder ORDER BY folder""", + (pst_id,), + ).fetchall() + return [dict(r) for r in rows] + + +def _escape_fts(query: str) -> str: + tokens = [] + for raw in query.split(): + if re.fullmatch(r"[A-Za-z0-9]+", raw): + tokens.append(f'"{raw}"*') + else: + sanitized = raw.replace('"', '""') + tokens.append(f'"{sanitized}"') + return " ".join(tokens) + + +@app.get("/api/pst-files/{pst_id}/messages") +def search_messages( + pst_id: str, + q: Optional[str] = None, + folder: Optional[str] = None, + limit: int = 50, + offset: int = 0, + user: User = Depends(require_app_user), +): + if not user_can_see_pst(user, pst_id): + raise HTTPException(404, "PST not found") + limit = max(1, min(limit, 200)) + offset = max(0, offset) + + with get_db() as conn: + p = conn.execute("SELECT id FROM pst_files WHERE id = ?", (pst_id,)).fetchone() + if not p: + raise HTTPException(404, "PST not found") + + params = [pst_id] + + if q and q.strip(): + fts_q = _escape_fts(q.strip()) + base = """ + FROM messages m + JOIN messages_fts f ON f.rowid = m.id + WHERE m.pst_id = ? AND messages_fts MATCH ? + """ + params.append(fts_q) + if folder: + base += " AND m.folder = ?" + params.append(folder) + + total = conn.execute("SELECT COUNT(*) as c " + base, params).fetchone()["c"] + + data_sql = ( + "SELECT m.id, m.folder, m.subject, m.sender, m.recipients, m.sent_date, " + " m.attachments, snippet(messages_fts, 3, '', '', '…', 24) AS snippet " + + base + " ORDER BY rank LIMIT ? OFFSET ?" + ) + rows = conn.execute(data_sql, params + [limit, offset]).fetchall() + else: + where = ["m.pst_id = ?"] + if folder: + where.append("m.folder = ?") + params.append(folder) + where_sql = " AND ".join(where) + total = conn.execute( + f"SELECT COUNT(*) as c FROM messages m WHERE {where_sql}", params + ).fetchone()["c"] + rows = conn.execute( + f"""SELECT m.id, m.folder, m.subject, m.sender, m.recipients, + m.sent_date, m.attachments, substr(m.body, 1, 200) AS snippet + FROM messages m WHERE {where_sql} + ORDER BY m.sent_date DESC LIMIT ? OFFSET ?""", + params + [limit, offset], + ).fetchall() + + return {"total": total, "limit": limit, "offset": offset, + "items": [dict(r) for r in rows]} + + +@app.get("/api/messages/{message_id}") +def get_message(message_id: int, user: User = Depends(require_app_user)): + with get_db() as conn: + row = conn.execute( + """SELECT id, pst_id, folder, subject, sender, recipients, + sent_date, body, attachments + FROM messages WHERE id = ?""", + (message_id,), + ).fetchone() + if not row: + raise HTTPException(404, "Message not found") + if not user_can_see_pst(user, row["pst_id"]): + raise HTTPException(404, "Message not found") + return dict(row) + + +# =========================================================================== +# ADMINISTRATION SECTION +# +# Previously a separate FastAPI process (admin_app.py) on its own port. It now +# runs inside the main application. Every endpoint below is namespaced under +# /api/admin and protected by `require_app_admin`, which authenticates against +# the main app session cookie AND requires the admin role. Non-admin users +# receive 403; unauthenticated requests receive 401. +# +# The administration UI is served at /admin (static files), and the main app +# shows an "ADMINISTRATION" button (admins only) that links to it. +# =========================================================================== + +# ----- Admin: user management ----- + +class CreateUserPayload(BaseModel): + username: str + password: str + role: str = "user" + + +class UpdatePasswordPayload(BaseModel): + password: str + + +class UpdateRolePayload(BaseModel): + role: str + + +@app.get("/api/admin/me") +def admin_me(user: User = Depends(require_app_admin)): + return {"user": user.public_dict()} + + +@app.get("/api/admin/users") +def admin_list_users(_: User = Depends(require_app_admin)): + return [u.public_dict() for u in list_users()] + + +@app.post("/api/admin/users") +def admin_create_user(payload: CreateUserPayload, + admin: User = Depends(require_app_admin)): + try: + user = create_user(payload.username, payload.password, role=payload.role) + except ValueError as e: + raise HTTPException(400, str(e)) + return user.public_dict() + + +@app.delete("/api/admin/users/{user_id}") +def admin_delete_user(user_id: str, admin: User = Depends(require_app_admin)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + if target.id == admin.id: + raise HTTPException(400, "You cannot delete your own account") + if target.is_admin and count_admins() <= 1: + raise HTTPException(400, "Cannot delete the last remaining admin") + delete_user(user_id) + return {"deleted": user_id} + + +@app.put("/api/admin/users/{user_id}/role") +def admin_set_role(user_id: str, payload: UpdateRolePayload, + admin: User = Depends(require_app_admin)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + if target.id == admin.id and payload.role != "admin": + raise HTTPException(400, "You cannot demote your own account") + if (target.is_admin and payload.role != "admin" and count_admins() <= 1): + raise HTTPException(400, "Cannot demote the last remaining admin") + try: + set_user_role(user_id, payload.role) + except ValueError as e: + raise HTTPException(400, str(e)) + return find_user_by_id(user_id).public_dict() + + +@app.put("/api/admin/users/{user_id}/password") +def admin_update_password(user_id: str, payload: UpdatePasswordPayload, + admin: User = Depends(require_app_admin)): + if not find_user_by_id(user_id): + raise HTTPException(404, "User not found") + try: + update_password(user_id, payload.password) + except ValueError as e: + raise HTTPException(400, str(e)) + return {"ok": True} + + +# ----- Admin: MFA management ----- + +class ConfirmMfaPayload(BaseModel): + totp_code: str + + +@app.post("/api/admin/users/{user_id}/mfa/begin") +def admin_begin_mfa(user_id: str, admin: User = Depends(require_app_admin)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + secret, uri = begin_mfa_enrollment(user_id) + return { + "secret": secret, + "otpauth_uri": uri, + "username": target.username, + "qr_url": f"/api/admin/users/{user_id}/mfa/qr", + } + + +@app.get("/api/admin/users/{user_id}/mfa/qr") +def admin_mfa_qr(user_id: str, admin: User = Depends(require_app_admin)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + secret = get_totp_secret(user_id) + if not secret: + raise HTTPException(400, "No MFA enrollment in progress") + uri = totp_provisioning_uri(secret, target.username) + + qr = qrcode.QRCode(box_size=6, border=2) + qr.add_data(uri) + qr.make(fit=True) + img = qr.make_image(fill_color="black", back_color="white") + buf = io.BytesIO() + img.save(buf, format="PNG") + buf.seek(0) + return StreamingResponse(buf, media_type="image/png") + + +@app.post("/api/admin/users/{user_id}/mfa/confirm") +def admin_confirm_mfa(user_id: str, payload: ConfirmMfaPayload, + admin: User = Depends(require_app_admin)): + if not find_user_by_id(user_id): + raise HTTPException(404, "User not found") + if not confirm_mfa_enrollment(user_id, payload.totp_code): + raise HTTPException(400, "Invalid MFA code — enrollment not confirmed") + return find_user_by_id(user_id).public_dict() + + +@app.post("/api/admin/users/{user_id}/mfa/disable") +def admin_disable_mfa(user_id: str, admin: User = Depends(require_app_admin)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + disable_mfa(user_id) + return find_user_by_id(user_id).public_dict() + + +# ----- Admin: folder management (sees ALL folders unconditionally) ----- + +class AdminFolderPayload(BaseModel): + name: str + + +@app.get("/api/admin/folders") +def admin_list_all_folders(_: User = Depends(require_app_admin)): + with get_db() as conn: + rows = conn.execute( + """SELECT f.id, f.name, f.created_at, f.created_by, + COUNT(p.id) AS file_count + FROM pst_folders f + LEFT JOIN pst_files p ON p.folder_id = f.id + GROUP BY f.id + ORDER BY f.name COLLATE NOCASE""" + ).fetchall() + return [dict(r) for r in rows] + + +@app.post("/api/admin/folders") +def admin_create_folder(payload: AdminFolderPayload, + admin: User = Depends(require_app_admin)): + name = _validate_folder_name(payload.name) + with get_db() as conn: + existing = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE", (name,) + ).fetchone() + if existing: + raise HTTPException(409, "A folder with that name already exists") + folder_id = uuid.uuid4().hex + conn.execute( + """INSERT INTO pst_folders (id, name, created_at, created_by) + VALUES (?, ?, ?, ?)""", + (folder_id, name, datetime.now(timezone.utc).isoformat(), admin.id), + ) + conn.commit() + return {"id": folder_id, "name": name, "file_count": 0} + + +@app.put("/api/admin/folders/{folder_id}") +def admin_rename_folder(folder_id: str, payload: AdminFolderPayload, + admin: User = Depends(require_app_admin)): + name = _validate_folder_name(payload.name) + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + clash = conn.execute( + "SELECT id FROM pst_folders WHERE name = ? COLLATE NOCASE AND id != ?", + (name, folder_id), + ).fetchone() + if clash: + raise HTTPException(409, "A folder with that name already exists") + conn.execute("UPDATE pst_folders SET name = ? WHERE id = ?", + (name, folder_id)) + conn.commit() + return {"id": folder_id, "name": name} + + +@app.delete("/api/admin/folders/{folder_id}") +def admin_delete_folder(folder_id: str, admin: User = Depends(require_app_admin)): + """Delete a folder. PSTs inside it become uncategorized (folder_id = NULL).""" + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = NULL WHERE folder_id = ?", + (folder_id,)) + conn.execute("DELETE FROM folder_permissions WHERE folder_id = ?", + (folder_id,)) + conn.execute("DELETE FROM pst_folders WHERE id = ?", (folder_id,)) + conn.commit() + return {"deleted": folder_id} + + +# ----- Admin: PST file management (sees ALL files unconditionally) ----- + +@app.get("/api/admin/pst-files") +def admin_list_pst_files(_: User = Depends(require_app_admin)): + with get_db() as conn: + rows = conn.execute( + """SELECT p.id, p.original_name, p.uploaded_at, p.size_bytes, + p.status, p.message_count, p.error, + p.folder_id, f.name AS folder_name, + p.uploaded_by + FROM pst_files p + LEFT JOIN pst_folders f ON f.id = p.folder_id + ORDER BY p.uploaded_at DESC""" + ).fetchall() + return [dict(r) for r in rows] + + +@app.delete("/api/admin/pst-files/{pst_id}") +def admin_delete_pst(pst_id: str, admin: User = Depends(require_app_admin)): + with get_db() as conn: + row = conn.execute( + "SELECT filename FROM pst_files WHERE id = ?", (pst_id,) + ).fetchone() + if not row: + raise HTTPException(404, "PST not found") + conn.execute("DELETE FROM messages WHERE pst_id = ?", (pst_id,)) + conn.execute("DELETE FROM pst_files WHERE id = ?", (pst_id,)) + conn.commit() + try: + (UPLOAD_DIR / row["filename"]).unlink(missing_ok=True) + except Exception: + pass + return {"deleted": pst_id} + + +class AdminAssignPayload(BaseModel): + folder_id: Optional[str] = None + + +@app.put("/api/admin/pst-files/{pst_id}/folder") +def admin_assign_pst_to_folder(pst_id: str, payload: AdminAssignPayload, + admin: User = Depends(require_app_admin)): + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_files WHERE id = ?", + (pst_id,)).fetchone(): + raise HTTPException(404, "PST not found") + if payload.folder_id: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (payload.folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + conn.execute("UPDATE pst_files SET folder_id = ? WHERE id = ?", + (payload.folder_id, pst_id)) + conn.commit() + return {"pst_id": pst_id, "folder_id": payload.folder_id} + + +# ----- Admin: folder access permissions ----- + +class PermissionToggle(BaseModel): + granted: bool + + +@app.get("/api/admin/permissions") +def admin_list_permissions(_: User = Depends(require_app_admin)): + """Return the full matrix: users, folders, and all grants.""" + with get_db() as conn: + users = conn.execute("SELECT * FROM users ORDER BY created_at ASC").fetchall() + folders = conn.execute( + "SELECT id, name FROM pst_folders ORDER BY name COLLATE NOCASE" + ).fetchall() + grants = conn.execute( + "SELECT user_id, folder_id, granted_at FROM folder_permissions" + ).fetchall() + + from auth import _row_to_user + user_list = [_row_to_user(u).public_dict() for u in users] + return { + "users": user_list, + "folders": [dict(f) for f in folders], + "grants": [dict(g) for g in grants], + } + + +@app.get("/api/admin/users/{user_id}/folders") +def admin_user_folders(user_id: str, _: User = Depends(require_app_admin)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + with get_db() as conn: + rows = conn.execute( + """SELECT f.id, f.name, p.granted_at + FROM folder_permissions p + JOIN pst_folders f ON f.id = p.folder_id + WHERE p.user_id = ? + ORDER BY f.name COLLATE NOCASE""", + (user_id,), + ).fetchall() + return {"user": target.public_dict(), "folders": [dict(r) for r in rows]} + + +@app.get("/api/admin/folders/{folder_id}/users") +def admin_folder_users(folder_id: str, _: User = Depends(require_app_admin)): + with get_db() as conn: + folder = conn.execute( + "SELECT id, name FROM pst_folders WHERE id = ?", (folder_id,) + ).fetchone() + if not folder: + raise HTTPException(404, "Folder not found") + grant_rows = conn.execute( + """SELECT user_id, granted_at + FROM folder_permissions WHERE folder_id = ?""", + (folder_id,), + ).fetchall() + + users_out = [] + for r in grant_rows: + u = find_user_by_id(r["user_id"]) + if u: + users_out.append({**u.public_dict(), "granted_at": r["granted_at"]}) + users_out.sort(key=lambda x: x["username"].lower()) + return {"folder": dict(folder), "users": users_out} + + +@app.put("/api/admin/permissions/{user_id}/{folder_id}") +def admin_toggle_permission(user_id: str, folder_id: str, + payload: PermissionToggle, + admin: User = Depends(require_app_admin)): + target = find_user_by_id(user_id) + if not target: + raise HTTPException(404, "User not found") + if target.is_admin: + raise HTTPException( + 400, + "Admins have access to all folders automatically; permissions " + "only apply to non-admin users.", + ) + with get_db() as conn: + if not conn.execute("SELECT id FROM pst_folders WHERE id = ?", + (folder_id,)).fetchone(): + raise HTTPException(404, "Folder not found") + if payload.granted: + conn.execute( + """INSERT OR IGNORE INTO folder_permissions + (user_id, folder_id, granted_at, granted_by) + VALUES (?, ?, ?, ?)""", + (user_id, folder_id, + datetime.now(timezone.utc).isoformat(), admin.id), + ) + else: + conn.execute( + "DELETE FROM folder_permissions WHERE user_id = ? AND folder_id = ?", + (user_id, folder_id), + ) + conn.commit() + return {"user_id": user_id, "folder_id": folder_id, "granted": payload.granted} + + +# --------------------------------------------------------------------------- +# Static frontends +# +# The administration UI is mounted at /admin and MUST be registered before the +# catch-all "/" mount so that /admin/* resolves to the admin assets. Both are +# served from the same origin/port, so they share the main session cookie. +# --------------------------------------------------------------------------- +if ADMIN_FRONTEND_DIR.exists(): + app.mount("/admin", StaticFiles(directory=str(ADMIN_FRONTEND_DIR), html=True), name="admin") + +# --------------------------------------------------------------------------- +# Static frontend (anonymous access — but index.html enforces auth client-side +# AND the APIs enforce it server-side, which is what actually matters) +# --------------------------------------------------------------------------- +if FRONTEND_DIR.exists(): + app.mount("/", StaticFiles(directory=str(FRONTEND_DIR), html=True), name="frontend") diff --git a/backend/auth.py b/backend/auth.py new file mode 100644 index 0000000..9937c82 --- /dev/null +++ b/backend/auth.py @@ -0,0 +1,520 @@ +""" +auth.py — Shared authentication & encryption module. + +Responsibilities: +- Derive or generate a master secret key (persisted to disk with chmod 600) +- Provide Fernet-based encryption for user PII at rest +- Argon2id password hashing (constant-time verification) +- TOTP secret generation, QR provisioning URI, 6-digit code verification +- JWT session tokens (signed with master key) +- User CRUD on the shared SQLite database +- FastAPI dependencies to enforce authentication and admin role +""" +from __future__ import annotations + +import base64 +import hashlib +import hmac +import os +import secrets +import sqlite3 +import time +from contextlib import contextmanager +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Optional + +import jwt +import pyotp +from argon2 import PasswordHasher +from argon2.exceptions import VerifyMismatchError, InvalidHashError +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from fastapi import Depends, HTTPException, Request, status + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- +DATA_DIR = Path(os.environ.get("DATA_DIR", "/app/data")) +DATA_DIR.mkdir(parents=True, exist_ok=True) + +SECRET_KEY_PATH = DATA_DIR / "secret.key" +DB_PATH = DATA_DIR / "pst_index.db" + +SESSION_TTL_SECONDS = int(os.environ.get("SESSION_TTL_SECONDS", 60 * 60 * 8)) # 8 hours +SESSION_COOKIE_NAME = "pst_session" +ADMIN_SESSION_COOKIE_NAME = "pst_admin_session" + +JWT_ALGORITHM = "HS256" + +# --------------------------------------------------------------------------- +# Master secret +# --------------------------------------------------------------------------- +def _load_or_create_master_secret() -> bytes: + """Return a 32-byte master secret. Created on first run, persisted to disk.""" + # Allow overriding via env var (useful for orchestration); still persist so + # restarts without the env var keep working. + env_secret = os.environ.get("MASTER_SECRET") + if env_secret: + return hashlib.sha256(env_secret.encode("utf-8")).digest() + + if SECRET_KEY_PATH.exists(): + data = SECRET_KEY_PATH.read_bytes() + if len(data) == 32: + return data + # Legacy or truncated: rewrap + return hashlib.sha256(data).digest() + + secret = secrets.token_bytes(32) + SECRET_KEY_PATH.write_bytes(secret) + try: + os.chmod(SECRET_KEY_PATH, 0o600) + except Exception: + pass + return secret + + +MASTER_SECRET: bytes = _load_or_create_master_secret() + + +def _derive_subkey(info: bytes, length: int = 32) -> bytes: + """Derive a sub-key from the master secret using HKDF-SHA256.""" + return HKDF( + algorithm=hashes.SHA256(), + length=length, + salt=b"pst-indexer-salt-v1", + info=info, + ).derive(MASTER_SECRET) + + +# Key used for encrypting user PII at rest +_FERNET_KEY = base64.urlsafe_b64encode(_derive_subkey(b"fernet-user-pii")) +_FERNET = Fernet(_FERNET_KEY) + +# Key used for JWT signatures +_JWT_KEY = _derive_subkey(b"jwt-sessions") + +# Key used for deterministic username lookup hashes (so we can index without +# exposing plaintext usernames in the DB). +_USERNAME_HMAC_KEY = _derive_subkey(b"username-lookup") + + +# --------------------------------------------------------------------------- +# Encryption helpers +# --------------------------------------------------------------------------- +def encrypt(plaintext: str) -> str: + """Fernet-encrypt a string; returns base64 ciphertext.""" + if plaintext is None: + return "" + return _FERNET.encrypt(plaintext.encode("utf-8")).decode("utf-8") + + +def decrypt(ciphertext: str) -> str: + """Fernet-decrypt. Returns empty string for empty input.""" + if not ciphertext: + return "" + try: + return _FERNET.decrypt(ciphertext.encode("utf-8")).decode("utf-8") + except InvalidToken: + raise ValueError("Failed to decrypt — master key may have changed") + + +def username_lookup_hash(username: str) -> str: + """Deterministic HMAC-SHA256 over the normalized username, hex-encoded.""" + normalized = username.strip().lower().encode("utf-8") + return hmac.new(_USERNAME_HMAC_KEY, normalized, hashlib.sha256).hexdigest() + + +# --------------------------------------------------------------------------- +# Password hashing +# --------------------------------------------------------------------------- +_hasher = PasswordHasher( + time_cost=3, + memory_cost=64 * 1024, # 64 MiB + parallelism=2, +) + + +def hash_password(password: str) -> str: + return _hasher.hash(password) + + +def verify_password(stored_hash: str, password: str) -> bool: + try: + _hasher.verify(stored_hash, password) + return True + except (VerifyMismatchError, InvalidHashError): + return False + except Exception: + return False + + +# --------------------------------------------------------------------------- +# TOTP (MFA) +# --------------------------------------------------------------------------- +def generate_totp_secret() -> str: + return pyotp.random_base32() + + +def totp_provisioning_uri(secret: str, username: str, issuer: str = "PST Archive") -> str: + return pyotp.TOTP(secret).provisioning_uri(name=username, issuer_name=issuer) + + +def verify_totp(secret: str, code: str) -> bool: + if not secret or not code: + return False + try: + return pyotp.TOTP(secret).verify(code.strip(), valid_window=1) + except Exception: + return False + + +# --------------------------------------------------------------------------- +# Database +# --------------------------------------------------------------------------- +@contextmanager +def get_db(): + conn = sqlite3.connect(str(DB_PATH)) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + try: + yield conn + finally: + conn.close() + + +def init_auth_schema(): + with get_db() as conn: + conn.executescript(""" + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + username_hash TEXT NOT NULL UNIQUE, + username_enc TEXT NOT NULL, + password_hash_enc TEXT NOT NULL, + totp_secret_enc TEXT, + mfa_enabled INTEGER NOT NULL DEFAULT 0, + role TEXT NOT NULL DEFAULT 'user', + created_at TEXT NOT NULL, + last_login TEXT + ); + + CREATE INDEX IF NOT EXISTS idx_users_username_hash ON users(username_hash); + CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); + """) + conn.commit() + + +# --------------------------------------------------------------------------- +# User model +# --------------------------------------------------------------------------- +@dataclass +class User: + id: str + username: str + role: str + mfa_enabled: bool + created_at: str + last_login: Optional[str] + + @property + def is_admin(self) -> bool: + return self.role == "admin" + + def public_dict(self) -> dict: + return { + "id": self.id, + "username": self.username, + "role": self.role, + "mfa_enabled": self.mfa_enabled, + "created_at": self.created_at, + "last_login": self.last_login, + } + + +def _row_to_user(row: sqlite3.Row) -> User: + return User( + id=row["id"], + username=decrypt(row["username_enc"]), + role=row["role"], + mfa_enabled=bool(row["mfa_enabled"]), + created_at=row["created_at"], + last_login=row["last_login"], + ) + + +def count_users() -> int: + with get_db() as conn: + return conn.execute("SELECT COUNT(*) AS c FROM users").fetchone()["c"] + + +def count_admins() -> int: + with get_db() as conn: + return conn.execute("SELECT COUNT(*) AS c FROM users WHERE role='admin'").fetchone()["c"] + + +def find_user_by_username(username: str) -> Optional[tuple[User, str]]: + """Return (User, decrypted_password_hash) if found, else None.""" + uh = username_lookup_hash(username) + with get_db() as conn: + row = conn.execute( + "SELECT * FROM users WHERE username_hash = ?", (uh,) + ).fetchone() + if not row: + return None + user = _row_to_user(row) + try: + pw_hash = decrypt(row["password_hash_enc"]) + except ValueError: + return None + return user, pw_hash + + +def find_user_by_id(user_id: str) -> Optional[User]: + with get_db() as conn: + row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone() + if not row: + return None + return _row_to_user(row) + + +def get_totp_secret(user_id: str) -> Optional[str]: + with get_db() as conn: + row = conn.execute( + "SELECT totp_secret_enc FROM users WHERE id = ?", (user_id,) + ).fetchone() + if not row or not row["totp_secret_enc"]: + return None + try: + return decrypt(row["totp_secret_enc"]) + except ValueError: + return None + + +def list_users() -> list[User]: + with get_db() as conn: + rows = conn.execute( + "SELECT * FROM users ORDER BY created_at ASC" + ).fetchall() + return [_row_to_user(r) for r in rows] + + +def create_user(username: str, password: str, role: str = "user") -> User: + username = username.strip() + if not username or len(username) < 3: + raise ValueError("Username must be at least 3 characters") + if len(username) > 64: + raise ValueError("Username too long") + if role not in ("user", "admin"): + raise ValueError("Invalid role") + if len(password) < 8: + raise ValueError("Password must be at least 8 characters") + + uh = username_lookup_hash(username) + with get_db() as conn: + existing = conn.execute( + "SELECT id FROM users WHERE username_hash = ?", (uh,) + ).fetchone() + if existing: + raise ValueError("Username already taken") + + user_id = secrets.token_hex(16) + now = datetime.now(timezone.utc).isoformat() + pw_hash = hash_password(password) + + conn.execute( + """INSERT INTO users + (id, username_hash, username_enc, password_hash_enc, + totp_secret_enc, mfa_enabled, role, created_at, last_login) + VALUES (?, ?, ?, ?, NULL, 0, ?, ?, NULL)""", + (user_id, uh, encrypt(username), encrypt(pw_hash), role, now), + ) + conn.commit() + + return User( + id=user_id, username=username, role=role, mfa_enabled=False, + created_at=now, last_login=None, + ) + + +def delete_user(user_id: str) -> None: + with get_db() as conn: + # Also clean up any folder permission grants this user had + conn.execute("DELETE FROM folder_permissions WHERE user_id = ?", (user_id,)) + conn.execute("DELETE FROM users WHERE id = ?", (user_id,)) + conn.commit() + + +def set_user_role(user_id: str, role: str) -> None: + if role not in ("user", "admin"): + raise ValueError("Invalid role") + with get_db() as conn: + conn.execute("UPDATE users SET role = ? WHERE id = ?", (role, user_id)) + conn.commit() + + +def update_password(user_id: str, new_password: str) -> None: + if len(new_password) < 8: + raise ValueError("Password must be at least 8 characters") + pw_hash = hash_password(new_password) + with get_db() as conn: + conn.execute( + "UPDATE users SET password_hash_enc = ? WHERE id = ?", + (encrypt(pw_hash), user_id), + ) + conn.commit() + + +def begin_mfa_enrollment(user_id: str) -> tuple[str, str]: + """Generate a new TOTP secret (not yet enabled) and return (secret, provisioning_uri).""" + user = find_user_by_id(user_id) + if not user: + raise ValueError("User not found") + secret = generate_totp_secret() + # Stash it but leave mfa_enabled = 0 until confirmed + with get_db() as conn: + conn.execute( + "UPDATE users SET totp_secret_enc = ?, mfa_enabled = 0 WHERE id = ?", + (encrypt(secret), user_id), + ) + conn.commit() + uri = totp_provisioning_uri(secret, user.username) + return secret, uri + + +def confirm_mfa_enrollment(user_id: str, code: str) -> bool: + secret = get_totp_secret(user_id) + if not secret or not verify_totp(secret, code): + return False + with get_db() as conn: + conn.execute("UPDATE users SET mfa_enabled = 1 WHERE id = ?", (user_id,)) + conn.commit() + return True + + +def disable_mfa(user_id: str) -> None: + with get_db() as conn: + conn.execute( + "UPDATE users SET mfa_enabled = 0, totp_secret_enc = NULL WHERE id = ?", + (user_id,), + ) + conn.commit() + + +def record_login(user_id: str) -> None: + with get_db() as conn: + conn.execute( + "UPDATE users SET last_login = ? WHERE id = ?", + (datetime.now(timezone.utc).isoformat(), user_id), + ) + conn.commit() + + +# --------------------------------------------------------------------------- +# JWT sessions +# --------------------------------------------------------------------------- +def create_session_token(user: User, scope: str = "app") -> str: + """scope='app' for main app, scope='admin' for admin panel.""" + now = datetime.now(timezone.utc) + payload = { + "sub": user.id, + "role": user.role, + "scope": scope, + "iat": int(now.timestamp()), + "exp": int((now + timedelta(seconds=SESSION_TTL_SECONDS)).timestamp()), + } + return jwt.encode(payload, _JWT_KEY, algorithm=JWT_ALGORITHM) + + +def verify_session_token(token: str, expected_scope: str) -> Optional[dict]: + try: + payload = jwt.decode(token, _JWT_KEY, algorithms=[JWT_ALGORITHM]) + if payload.get("scope") != expected_scope: + return None + return payload + except jwt.PyJWTError: + return None + + +# --------------------------------------------------------------------------- +# FastAPI dependencies +# --------------------------------------------------------------------------- +def _extract_token(request: Request, cookie_name: str) -> Optional[str]: + # Prefer cookie, fall back to Authorization header + token = request.cookies.get(cookie_name) + if token: + return token + auth = request.headers.get("authorization", "") + if auth.lower().startswith("bearer "): + return auth[7:].strip() + return None + + +def require_user(scope: str, cookie_name: str): + def _dep(request: Request) -> User: + token = _extract_token(request, cookie_name) + if not token: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Not authenticated") + payload = verify_session_token(token, expected_scope=scope) + if not payload: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "Invalid or expired session") + user = find_user_by_id(payload["sub"]) + if not user: + raise HTTPException(status.HTTP_401_UNAUTHORIZED, "User no longer exists") + return user + return _dep + + +def require_admin(scope: str, cookie_name: str): + base = require_user(scope, cookie_name) + def _dep(request: Request) -> User: + user = base(request) + if not user.is_admin: + raise HTTPException(status.HTTP_403_FORBIDDEN, "Admin role required") + return user + return _dep + + +# Pre-wired dependencies for the two app scopes +require_app_user = require_user("app", SESSION_COOKIE_NAME) +require_admin_user = require_admin("admin", ADMIN_SESSION_COOKIE_NAME) + +# App-scoped admin dependency: authenticates against the MAIN app session +# cookie (scope "app") but additionally requires the admin role. This lets the +# administration section run inside the main application (served at /admin) +# instead of as a separate process on its own port, while still being strictly +# limited to admin users. +require_app_admin = require_admin("app", SESSION_COOKIE_NAME) + + +# --------------------------------------------------------------------------- +# Simple rate limiter (per-IP, per-username) to slow brute-force attempts +# --------------------------------------------------------------------------- +class RateLimiter: + def __init__(self, window_seconds: int = 300, max_attempts: int = 10): + self.window = window_seconds + self.max = max_attempts + self._attempts: dict[str, list[float]] = {} + + def check(self, key: str) -> bool: + now = time.time() + cutoff = now - self.window + arr = [t for t in self._attempts.get(key, []) if t > cutoff] + if len(arr) >= self.max: + self._attempts[key] = arr + return False + return True + + def record(self, key: str) -> None: + now = time.time() + cutoff = now - self.window + arr = [t for t in self._attempts.get(key, []) if t > cutoff] + arr.append(now) + self._attempts[key] = arr + + def reset(self, key: str) -> None: + self._attempts.pop(key, None) + + +login_limiter = RateLimiter(window_seconds=300, max_attempts=10) diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..9cbd286 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,10 @@ +fastapi==0.115.0 +uvicorn[standard]==0.32.0 +python-multipart==0.0.12 +libpff-python==20231205 +aiofiles==24.1.0 +argon2-cffi==23.1.0 +pyotp==2.9.0 +cryptography==43.0.1 +PyJWT==2.9.0 +qrcode[pil]==7.4.2 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d5504cc --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +services: + pst-indexer: + build: . + container_name: pst-indexer + ports: + - "8000:8000" # App + built-in administration (at /admin) + volumes: + - ./data:/app/data + restart: unless-stopped + environment: + - DATA_DIR=/app/data + # Set to "true" when serving over HTTPS + - COOKIE_SECURE=false + # Session lifetime in seconds (default 8 hours) + - SESSION_TTL_SECONDS=28800 + # Optional: provide your own master secret. If set, this becomes the + # root key for all encryption. If omitted, a random key is generated + # on first run and persisted in ./data/secret.key (chmod 600). + # - MASTER_SECRET=change-me-to-a-long-random-string diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 0000000..8e6b790 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# Start the application (port 8000). The administration section now runs inside +# this same app (served at /admin), so there is no longer a separate admin +# process or port. + +set -e + +cleanup() { + echo "Shutting down…" + kill -TERM "$MAIN_PID" 2>/dev/null || true + wait + exit 0 +} +trap cleanup SIGINT SIGTERM + +cd /app/backend + +echo "[start] App (with built-in administration) on :8000" +uvicorn app:app --host 0.0.0.0 --port 8000 --proxy-headers & +MAIN_PID=$! + +wait "$MAIN_PID" +EXIT_CODE=$? +echo "[exit] Service exited with code $EXIT_CODE" +exit $EXIT_CODE diff --git a/frontend/app.js b/frontend/app.js new file mode 100644 index 0000000..49cac5f --- /dev/null +++ b/frontend/app.js @@ -0,0 +1,877 @@ +/* ============================================================ + PST Archive — frontend logic + - Setup wizard (first run) + - Login (+ MFA) + - Main app: upload, index, search, view + ============================================================ */ + +const API = "/api"; + +const state = { + activePstId: null, + activePstName: "", + folder: "", + query: "", + limit: 25, + offset: 0, + total: 0, + refreshTimer: null, + user: null, + folders: [], // [{id, name, file_count}, ...] + psts: [], // last loaded pst list + collapsedFolders: new Set(), // folder IDs (or "__uncat__") that are collapsed + movePstId: null, // currently being moved +}; + +const $ = (id) => document.getElementById(id); + +// ---------- helpers ---------- +function fmtBytes(n) { + if (!n) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + let i = 0; + while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; } + return `${n.toFixed(i ? 1 : 0)} ${units[i]}`; +} + +function fmtDate(iso) { + if (!iso) return "—"; + try { + const d = new Date(iso); + if (isNaN(d)) return iso; + return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }); + } catch { return iso; } +} + +function fmtDateTime(iso) { + if (!iso) return "—"; + try { + const d = new Date(iso); + if (isNaN(d)) return iso; + return d.toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); + } catch { return iso; } +} + +function toast(msg, ms = 3000) { + const el = $("toast"); + el.textContent = msg; + el.classList.remove("hidden"); + clearTimeout(toast._t); + toast._t = setTimeout(() => el.classList.add("hidden"), ms); +} + +function escapeHtml(s) { + if (s == null) return ""; + return String(s) + .replace(/&/g, "&").replace(//g, ">") + .replace(/"/g, """).replace(/'/g, "'"); +} + +function renderSnippet(snippet) { + if (!snippet) return ""; + return escapeHtml(snippet) + .replace(/<mark>/g, "") + .replace(/<\/mark>/g, ""); +} + +function debounce(fn, ms) { + let t; + return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), ms); }; +} + +// ---------- API ---------- +async function api(path, opts = {}) { + const res = await fetch(API + path, { + credentials: "same-origin", + ...opts, + }); + let body = null; + try { body = await res.json(); } catch {} + if (!res.ok) { + const err = new Error((body && body.detail) || res.statusText); + err.status = res.status; + err.body = body || {}; + throw err; + } + return body; +} + +// ---------- Screen switching ---------- +function showScreen(screen) { + ["setupScreen", "loginScreen", "mainApp"].forEach((id) => { + $(id).classList.toggle("hidden", id !== screen); + }); +} + +// ---------- Bootstrap ---------- +async function bootstrap() { + // 1. Try to fetch current user + try { + const { user } = await api("/me"); + state.user = user; + enterApp(); + return; + } catch (e) { + if (e.status !== 401) { + showLogin(); + toast("Connection problem — please retry."); + return; + } + } + + // 2. Not logged in — check setup status + try { + const { needs_setup } = await api("/setup/status"); + if (needs_setup) showSetup(); + else showLogin(); + } catch { + showLogin(); + } +} + +// ---------- Setup ---------- +function showSetup() { + showScreen("setupScreen"); + $("setupForm").reset(); + $("setupError").classList.add("hidden"); + setTimeout(() => $("setupForm").username?.focus(), 50); +} + +async function handleSetup(e) { + e.preventDefault(); + const form = e.target; + const errEl = $("setupError"); + errEl.classList.add("hidden"); + + const username = form.username.value.trim(); + const password = form.password.value; + const password2 = form.password2.value; + + if (password !== password2) { + errEl.textContent = "Passwords do not match."; + errEl.classList.remove("hidden"); + return; + } + + try { + const { user } = await api("/setup", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + state.user = user; + toast("Welcome — admin account created."); + enterApp(); + } catch (err) { + errEl.textContent = err.message || "Setup failed."; + errEl.classList.remove("hidden"); + } +} + +// ---------- Login ---------- +function showLogin() { + showScreen("loginScreen"); + $("loginForm").reset(); + $("mfaField").classList.add("hidden"); + $("loginError").classList.add("hidden"); + setTimeout(() => $("loginForm").username?.focus(), 50); +} + +async function handleLogin(e) { + e.preventDefault(); + const form = e.target; + const errEl = $("loginError"); + errEl.classList.add("hidden"); + + const payload = { + username: form.username.value.trim(), + password: form.password.value, + }; + const mfaInput = form.totp_code.value.trim(); + if (mfaInput) payload.totp_code = mfaInput; + + try { + const { user } = await api("/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + state.user = user; + toast(`Welcome, ${user.username}.`); + enterApp(); + } catch (err) { + if (err.status === 401 && err.body && err.body.mfa_required) { + $("mfaField").classList.remove("hidden"); + const f = $("loginForm").totp_code; + f.value = ""; + setTimeout(() => f.focus(), 50); + errEl.textContent = "Enter your authenticator code to continue."; + errEl.classList.remove("hidden"); + return; + } + errEl.textContent = err.message || "Login failed."; + errEl.classList.remove("hidden"); + } +} + +async function handleLogout() { + try { await api("/logout", { method: "POST" }); } catch {} + state.user = null; + state.activePstId = null; + resetReader(); + showLogin(); +} + +// ---------- Enter the app ---------- +function enterApp() { + showScreen("mainApp"); + $("userChip").textContent = `${state.user.username}${state.user.role === "admin" ? " · admin" : ""}`; + // Show the ADMINISTRATION button only for admins. + const adminBtn = $("adminNavBtn"); + if (adminBtn) adminBtn.classList.toggle("hidden", state.user.role !== "admin"); + checkHealth(); + loadPstList(); + setInterval(checkHealth, 30000); +} + +// ---------- Health ---------- +async function checkHealth() { + const dot = $("statusDot"); + const text = $("statusText"); + try { + await api("/health"); + dot.className = "status-dot ok"; + text.textContent = "connected"; + } catch { + dot.className = "status-dot err"; + text.textContent = "offline"; + } +} + +// ---------- PST list ---------- +async function loadPstList() { + try { + const [folders, items] = await Promise.all([ + api("/folders"), + api("/pst-files"), + ]); + state.folders = folders; + state.psts = items; + renderPstList(); + if (items.some((i) => i.status === "indexing")) { + clearTimeout(state.refreshTimer); + state.refreshTimer = setTimeout(loadPstList, 2500); + } + } catch (e) { + if (e.status === 401) { handleLogout(); return; } + toast(`Failed to load archives: ${e.message}`); + } +} + +function pstItemHtml(i) { + const active = i.id === state.activePstId ? "active" : ""; + const disabled = i.status !== "ready"; + const statusText = i.status === "indexing" ? "indexing…" : + i.status === "ready" ? `${i.message_count.toLocaleString()} msgs` : + i.status === "failed" ? "failed" : i.status; + return ` +
  • +
    + ${escapeHtml(i.original_name)} + +
    +
    + ${fmtBytes(i.size_bytes)} · ${fmtDate(i.uploaded_at)} + ${statusText} +
    +
  • + `; +} + +function renderPstList() { + const ul = $("pstList"); + const { folders, psts } = state; + + if (!folders.length && !psts.length) { + ul.innerHTML = '
  • No archives yet.
  • '; + return; + } + + // Group PSTs by folder_id + const byFolder = new Map(); + byFolder.set(null, []); // uncategorized bucket + folders.forEach((f) => byFolder.set(f.id, [])); + psts.forEach((p) => { + const bucket = byFolder.has(p.folder_id) ? p.folder_id : null; + byFolder.get(bucket).push(p); + }); + + const parts = []; + + // Render user-defined folders first (sorted alphabetically — server does this) + for (const f of folders) { + const items = byFolder.get(f.id) || []; + const collapsed = state.collapsedFolders.has(f.id) ? "collapsed" : ""; + parts.push(` +
  • +
    +
    + + ${escapeHtml(f.name)} + ${items.length} +
    + +
    +
      + ${items.length + ? items.map(pstItemHtml).join("") + : '
    • — empty —
    • '} +
    +
  • + `); + } + + // Uncategorized group — only shown if it has items OR there are no folders + const uncat = byFolder.get(null) || []; + if (uncat.length || !folders.length) { + const collapsed = state.collapsedFolders.has("__uncat__") ? "collapsed" : ""; + parts.push(` +
  • +
    +
    + + Uncategorized + ${uncat.length} +
    +
    +
      + ${uncat.length + ? uncat.map(pstItemHtml).join("") + : '
    • — empty —
    • '} +
    +
  • + `); + } + + ul.innerHTML = parts.join(""); + + // PST click + ul.querySelectorAll(".pst-item").forEach((el) => { + el.addEventListener("click", (e) => { + if (e.target.closest("[data-pst-menu]")) return; + if (el.dataset.disabled) { + toast(el.dataset.status === "indexing" ? "Still indexing — one moment…" : "This archive is unavailable."); + return; + } + selectPst(el.dataset.id, el.dataset.name); + }); + }); + + // PST action menu + ul.querySelectorAll("[data-pst-menu]").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + openPstMenu(e, btn.dataset.pstMenu); + }); + }); + + // Folder header toggle (collapse/expand) + ul.querySelectorAll("[data-folder-header]").forEach((el) => { + el.addEventListener("click", (e) => { + if (e.target.closest("[data-folder-menu]")) return; + const id = el.dataset.folderHeader; + if (state.collapsedFolders.has(id)) state.collapsedFolders.delete(id); + else state.collapsedFolders.add(id); + const group = el.closest(".folder-group"); + group.classList.toggle("collapsed"); + }); + }); + + // Folder action menu + ul.querySelectorAll("[data-folder-menu]").forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + openFolderMenu(e, btn.dataset.folderMenu); + }); + }); +} + +// ---------- Context menus ---------- +function closeContextMenu() { + document.querySelectorAll(".context-menu").forEach((el) => el.remove()); +} + +function openContextMenu(x, y, items) { + closeContextMenu(); + const menu = document.createElement("div"); + menu.className = "context-menu"; + items.forEach((it) => { + if (it.divider) { + menu.appendChild(document.createElement("hr")); + return; + } + const btn = document.createElement("button"); + btn.textContent = it.label; + if (it.danger) btn.classList.add("danger"); + btn.addEventListener("click", () => { + closeContextMenu(); + it.onClick(); + }); + menu.appendChild(btn); + }); + document.body.appendChild(menu); + + // Position, keeping menu inside viewport + const rect = menu.getBoundingClientRect(); + const maxX = window.innerWidth - rect.width - 4; + const maxY = window.innerHeight - rect.height - 4; + menu.style.left = Math.min(x, maxX) + "px"; + menu.style.top = Math.min(y, maxY) + "px"; + + // Close on outside click or escape + setTimeout(() => { + document.addEventListener("click", closeContextMenu, { once: true }); + }, 0); +} + +function openPstMenu(event, pstId) { + const p = state.psts.find((x) => x.id === pstId); + if (!p) return; + const r = event.currentTarget.getBoundingClientRect(); + openContextMenu(r.left, r.bottom + 4, [ + { + label: "Move to folder…", + onClick: () => openMoveModal(p), + }, + { divider: true }, + { + label: "Delete archive", + danger: true, + onClick: () => deletePst(p), + }, + ]); +} + +function openFolderMenu(event, folderId) { + const f = state.folders.find((x) => x.id === folderId); + if (!f) return; + const r = event.currentTarget.getBoundingClientRect(); + openContextMenu(r.left, r.bottom + 4, [ + { + label: "Rename folder…", + onClick: () => renameFolder(f), + }, + { divider: true }, + { + label: "Delete folder", + danger: true, + onClick: () => deleteFolder(f), + }, + ]); +} + +// ---------- Folder operations ---------- +function openFolderModal() { + $("folderForm").reset(); + $("folderError").classList.add("hidden"); + $("folderModal").classList.remove("hidden"); + setTimeout(() => $("folderForm").name.focus(), 50); +} + +async function handleCreateFolder(e) { + e.preventDefault(); + const form = e.target; + const errEl = $("folderError"); + errEl.classList.add("hidden"); + try { + await api("/folders", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: form.name.value }), + }); + toast("Folder created."); + closeModals(); + loadPstList(); + } catch (err) { + errEl.textContent = err.message || "Create failed."; + errEl.classList.remove("hidden"); + } +} + +async function renameFolder(f) { + const newName = prompt(`Rename "${f.name}" to:`, f.name); + if (!newName || newName.trim() === f.name) return; + try { + await api(`/folders/${f.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: newName }), + }); + toast("Folder renamed."); + loadPstList(); + } catch (err) { + toast(`Rename failed: ${err.message}`); + } +} + +async function deleteFolder(f) { + if (!confirm(`Delete folder "${f.name}"?\n\nAny archives inside will be moved to Uncategorized.`)) return; + try { + await api(`/folders/${f.id}`, { method: "DELETE" }); + toast("Folder deleted."); + loadPstList(); + } catch (err) { + toast(`Delete failed: ${err.message}`); + } +} + +async function deletePst(p) { + if (!confirm(`Remove "${p.original_name}" and all indexed messages?`)) return; + try { + await api(`/pst-files/${p.id}`, { method: "DELETE" }); + if (state.activePstId === p.id) { state.activePstId = null; resetReader(); } + toast("Archive removed."); + loadPstList(); + } catch (err) { + toast(`Delete failed: ${err.message}`); + } +} + +function openMoveModal(p) { + state.movePstId = p.id; + $("moveSubject").textContent = p.original_name; + + const sel = $("moveFolderSelect"); + sel.innerHTML = '' + + state.folders.map((f) => + `` + ).join(""); + if (!p.folder_id) sel.value = ""; + + $("moveError").classList.add("hidden"); + $("moveModal").classList.remove("hidden"); +} + +async function handleMovePst(e) { + e.preventDefault(); + const errEl = $("moveError"); + errEl.classList.add("hidden"); + const folderId = $("moveFolderSelect").value || null; + try { + await api(`/pst-files/${state.movePstId}/folder`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ folder_id: folderId }), + }); + toast("Moved."); + closeModals(); + loadPstList(); + } catch (err) { + errEl.textContent = err.message || "Move failed."; + errEl.classList.remove("hidden"); + } +} + +function closeModals() { + document.querySelectorAll(".modal").forEach((m) => m.classList.add("hidden")); +} + +// ---------- Selection + folders ---------- +async function selectPst(id, name) { + state.activePstId = id; + state.activePstName = name; + state.folder = ""; + state.query = ""; + state.offset = 0; + + $("searchInput").value = ""; + $("searchInput").disabled = false; + $("folderFilter").disabled = false; + + document.querySelectorAll(".pst-item").forEach((el) => { + el.classList.toggle("active", el.dataset.id === id); + }); + $("readerTitle").textContent = name; + $("readerMeta").textContent = "Loading messages…"; + hideMessageView(); + await Promise.all([loadFolders(), loadMessages()]); +} + +async function loadFolders() { + if (!state.activePstId) return; + try { + const folders = await api(`/pst-files/${state.activePstId}/folders`); + const sel = $("folderFilter"); + sel.innerHTML = + '' + + folders.map((f) => ``).join(""); + sel.value = state.folder || ""; + } catch (e) { console.warn("folders", e); } +} + +// ---------- Messages ---------- +async function loadMessages() { + if (!state.activePstId) return; + const params = new URLSearchParams(); + if (state.query) params.set("q", state.query); + if (state.folder) params.set("folder", state.folder); + params.set("limit", state.limit); + params.set("offset", state.offset); + + try { + const res = await api(`/pst-files/${state.activePstId}/messages?${params}`); + state.total = res.total; + renderMessages(res.items); + renderPaginator(); + $("readerMeta").textContent = + `${state.total.toLocaleString()} message${state.total === 1 ? "" : "s"}` + + (state.query ? ` matching "${state.query}"` : "") + + (state.folder ? ` in ${state.folder}` : ""); + } catch (e) { + if (e.status === 401) { handleLogout(); return; } + toast(`Search failed: ${e.message}`); + } +} + +function renderMessages(items) { + const list = $("messageList"); + if (!items.length) { + list.innerHTML = ` +
    +
    +

    No messages match.

    +
    `; + return; + } + list.innerHTML = items.map((m) => { + const hasAtt = m.attachments && m.attachments.trim().length > 0; + return ` +
    +
    ${escapeHtml(m.sender || "(unknown sender)")}
    +
    +
    ${escapeHtml(m.subject || "(no subject)")} ${hasAtt ? '' : ""}
    +
    ${renderSnippet(m.snippet || "")}
    +
    +
    +
    ${fmtDate(m.sent_date)}
    +
    ${escapeHtml((m.folder || "").split("/").filter(Boolean).pop() || "inbox")}
    +
    +
    `; + }).join(""); + list.querySelectorAll(".message-row").forEach((el) => { + el.addEventListener("click", () => openMessage(el.dataset.id)); + }); +} + +function renderPaginator() { + const p = $("paginator"); + if (state.total <= state.limit) { p.classList.add("hidden"); return; } + p.classList.remove("hidden"); + const from = state.offset + 1; + const to = Math.min(state.offset + state.limit, state.total); + $("pageInfo").textContent = `${from}–${to} of ${state.total.toLocaleString()}`; + $("prevBtn").disabled = state.offset === 0; + $("nextBtn").disabled = state.offset + state.limit >= state.total; +} + +// ---------- Message view ---------- +async function openMessage(id) { + try { + const m = await api(`/messages/${id}`); + $("mvSubject").textContent = m.subject || "(no subject)"; + $("mvSender").textContent = m.sender || "—"; + $("mvRecipients").textContent = m.recipients || "—"; + $("mvDate").textContent = fmtDateTime(m.sent_date); + $("mvFolder").textContent = m.folder || "—"; + if (m.attachments && m.attachments.trim()) { + $("mvAttLabel").classList.remove("hidden"); + $("mvAttachments").classList.remove("hidden"); + $("mvAttachments").textContent = m.attachments.split(" | ").join(", "); + } else { + $("mvAttLabel").classList.add("hidden"); + $("mvAttachments").classList.add("hidden"); + } + $("mvBody").textContent = m.body || "(empty body)"; + $("messageView").classList.remove("hidden"); + } catch (e) { + if (e.status === 401) { handleLogout(); return; } + toast(`Could not open message: ${e.message}`); + } +} + +function hideMessageView() { $("messageView").classList.add("hidden"); } + +function resetReader() { + $("readerTitle").textContent = "Select an archive"; + $("readerMeta").textContent = "Upload a PST file or select one from the left to begin."; + $("searchInput").disabled = true; + $("folderFilter").disabled = true; + $("messageList").innerHTML = ` +
    +
    +

    The archive awaits.

    +
    `; + $("paginator").classList.add("hidden"); + hideMessageView(); +} + +// ---------- Upload ---------- +function uploadFile(file) { + if (!file) return; + const lower = file.name.toLowerCase(); + if (!lower.endsWith(".pst") && !lower.endsWith(".ost")) { + toast("Only .pst and .ost files are accepted."); + return; + } + const progress = $("uploadProgress"); + const bar = $("progressBar"); + const pct = $("progressPct"); + const nm = $("progressName"); + progress.classList.remove("hidden"); + nm.textContent = file.name; + bar.style.width = "0%"; + pct.textContent = "0%"; + + const form = new FormData(); + form.append("file", file); + + const xhr = new XMLHttpRequest(); + xhr.open("POST", API + "/pst-files"); + xhr.withCredentials = true; + xhr.upload.onprogress = (e) => { + if (e.lengthComputable) { + const p = Math.round((e.loaded / e.total) * 100); + bar.style.width = p + "%"; + pct.textContent = p + "%"; + } + }; + xhr.onload = () => { + if (xhr.status >= 200 && xhr.status < 300) { + pct.textContent = "indexing…"; + bar.style.width = "100%"; + toast("Upload complete. Indexing started in background."); + setTimeout(() => progress.classList.add("hidden"), 2500); + loadPstList(); + } else if (xhr.status === 401) { + handleLogout(); + } else { + let msg = "Upload failed"; + try { msg = JSON.parse(xhr.responseText).detail || msg; } catch {} + toast(msg); + progress.classList.add("hidden"); + } + }; + xhr.onerror = () => { toast("Upload failed (network)."); progress.classList.add("hidden"); }; + xhr.send(form); +} + +// ---------- Theme ---------- +function initTheme() { + // Preference order: localStorage → prefers-color-scheme → light + let theme = "light"; + try { + const saved = localStorage.getItem("pst-theme"); + if (saved === "light" || saved === "dark") { + theme = saved; + } else if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { + theme = "dark"; + } + } catch { /* ignore storage errors */ } + document.documentElement.dataset.theme = theme; +} + +function toggleTheme() { + const current = document.documentElement.dataset.theme === "dark" ? "dark" : "light"; + const next = current === "dark" ? "light" : "dark"; + document.documentElement.dataset.theme = next; + try { localStorage.setItem("pst-theme", next); } catch {} +} + +// Apply theme immediately so there's no flash of wrong theme +initTheme(); + +// ---------- Wire up ---------- +function init() { + $("setupForm").addEventListener("submit", handleSetup); + $("loginForm").addEventListener("submit", handleLogin); + $("logoutBtn").addEventListener("click", handleLogout); + + const adminNavBtn = $("adminNavBtn"); + if (adminNavBtn) { + adminNavBtn.addEventListener("click", () => { window.location.href = "/admin/"; }); + } + + // Theme toggles (both the in-masthead and floating variants) + ["themeToggle", "themeToggleFloat"].forEach((id) => { + const el = document.getElementById(id); + if (el) el.addEventListener("click", toggleTheme); + }); + + $("fileInput").addEventListener("change", (e) => { + const f = e.target.files[0]; + if (f) uploadFile(f); + e.target.value = ""; + }); + + const dz = $("dropzone"); + ["dragenter", "dragover"].forEach((ev) => { + dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.add("drag-over"); }); + }); + ["dragleave", "drop"].forEach((ev) => { + dz.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.remove("drag-over"); }); + }); + dz.addEventListener("drop", (e) => { + const f = e.dataTransfer.files?.[0]; + if (f) uploadFile(f); + }); + + $("refreshBtn").addEventListener("click", loadPstList); + $("newFolderBtn").addEventListener("click", openFolderModal); + $("folderForm").addEventListener("submit", handleCreateFolder); + $("moveForm").addEventListener("submit", handleMovePst); + + // Modal close buttons + backdrop clicks + Escape + document.querySelectorAll("[data-close]").forEach((el) => { + el.addEventListener("click", closeModals); + }); + document.querySelectorAll(".modal").forEach((m) => { + m.addEventListener("click", (e) => { + if (e.target === m) closeModals(); + }); + }); + + const searchHandler = debounce(() => { + state.query = $("searchInput").value.trim(); + state.offset = 0; + loadMessages(); + }, 250); + $("searchInput").addEventListener("input", searchHandler); + + $("folderFilter").addEventListener("change", () => { + state.folder = $("folderFilter").value; + state.offset = 0; + loadMessages(); + }); + + $("prevBtn").addEventListener("click", () => { + state.offset = Math.max(0, state.offset - state.limit); + loadMessages(); + }); + $("nextBtn").addEventListener("click", () => { + state.offset += state.limit; + loadMessages(); + }); + + $("closeMessageBtn").addEventListener("click", hideMessageView); + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + hideMessageView(); + closeModals(); + closeContextMenu(); + } + }); + + bootstrap(); +} + +document.addEventListener("DOMContentLoaded", init); diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..76b9585 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,259 @@ + + + + + +PST Archive · Mail Indexer + + + + + + + +
    + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/styles.css b/frontend/styles.css new file mode 100644 index 0000000..63db6c0 --- /dev/null +++ b/frontend/styles.css @@ -0,0 +1,1182 @@ +/* ============================================================ + PST ARCHIVE — aesthetic: editorial/forensic archive + Warm paper palette, serif display, mono accents, grain overlay + Supports light (default) and dark themes via [data-theme] + ============================================================ */ + +:root, +:root[data-theme="light"] { + --paper: #f2ede3; + --paper-deep: #e8e1d3; + --ink: #1b1814; + --ink-soft: #3a332a; + --ink-muted: #6b6258; + --rule: #c9bfad; + --rule-soft: #dcd3c1; + --accent: #a8341f; /* oxblood red */ + --accent-soft: #c95a42; + --highlight: #e8d67a; /* faded yellow marker */ + --ok: #2e6f3a; + --warn: #a86b1e; + --shadow: 0 1px 0 rgba(27, 24, 20, 0.04), 0 20px 40px -20px rgba(27, 24, 20, 0.25); + --grain-opacity: 0.35; + + --font-display: "Fraunces", "Iowan Old Style", "Palatino", Georgia, serif; + --font-mono: "JetBrains Mono", "SFMono-Regular", ui-monospace, monospace; +} + +:root[data-theme="dark"] { + --paper: #17140f; /* deep inked paper */ + --paper-deep: #201c16; /* slightly lighter ink-pool for surfaces */ + --ink: #e9e0cb; /* warm paper-cream as the ink color */ + --ink-soft: #c4b99f; + --ink-muted: #8a8170; + --rule: #3a3226; + --rule-soft: #2a251d; + --accent: #d06347; /* softened oxblood that holds up on dark */ + --accent-soft: #e4876c; + --highlight: #d8b64a; /* brass-yellow marker */ + --ok: #6fae7c; + --warn: #d99a4a; + --shadow: 0 1px 0 rgba(0, 0, 0, 0.4), 0 20px 40px -20px rgba(0, 0, 0, 0.9); + --grain-opacity: 0.12; +} + +* { box-sizing: border-box; } + +html, body { + margin: 0; + padding: 0; + height: 100%; + background: var(--paper); + color: var(--ink); + font-family: var(--font-display); + font-feature-settings: "ss01", "onum"; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + display: flex; + flex-direction: column; + min-height: 100vh; + background-image: + radial-gradient(circle at 10% 10%, rgba(168, 52, 31, 0.04), transparent 40%), + radial-gradient(circle at 90% 90%, rgba(74, 58, 42, 0.05), transparent 50%); +} + +:root[data-theme="dark"] body { + background-image: + radial-gradient(circle at 10% 10%, rgba(208, 99, 71, 0.06), transparent 40%), + radial-gradient(circle at 90% 90%, rgba(216, 182, 74, 0.03), transparent 50%); +} + +/* grain overlay */ +.grain { + pointer-events: none; + position: fixed; + inset: 0; + z-index: 9999; + opacity: var(--grain-opacity); + mix-blend-mode: multiply; + background-image: url("data:image/svg+xml;utf8,"); +} + +:root[data-theme="dark"] .grain { + mix-blend-mode: screen; +} + +/* ============================================================ + MASTHEAD + ============================================================ */ +.masthead { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 28px 40px 18px; + border-bottom: 1px solid var(--ink); + position: relative; +} + +.masthead::after { + content: ""; + position: absolute; + bottom: -4px; + left: 40px; + right: 40px; + height: 1px; + background: var(--ink); +} + +.masthead-left { display: flex; align-items: center; gap: 18px; } + +.logo-mark { + color: var(--ink); + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border: 1px solid var(--ink); + background: var(--paper); +} + +.masthead-title h1 { + margin: 0; + font-size: 2.5rem; + font-weight: 500; + letter-spacing: -0.02em; + line-height: 1; + font-variation-settings: "SOFT" 30, "WONK" 1; +} + +.tagline { + margin: 6px 0 0; + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.masthead-right { + display: flex; + align-items: center; + gap: 10px; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--warn); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--warn) 25%, transparent); +} +.status-dot.ok { background: var(--ok); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ok) 25%, transparent); } +.status-dot.err { background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent); } + +/* ============================================================ + WORKSPACE + ============================================================ */ +.workspace { + display: grid; + grid-template-columns: 340px 1fr; + flex: 1; + min-height: 0; +} + +/* ============================================================ + SIDEBAR + ============================================================ */ +.sidebar { + border-right: 1px solid var(--rule); + padding: 28px 24px; + display: flex; + flex-direction: column; + gap: 36px; + overflow-y: auto; + background: linear-gradient(180deg, rgba(232, 225, 211, 0.4), transparent 40%); +} + +.section-heading { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--ink-soft); + margin: 0 0 14px; + padding-bottom: 8px; + border-bottom: 1px solid var(--rule); + display: flex; + align-items: baseline; + justify-content: space-between; +} + +.dropzone { + display: block; + border: 1.5px dashed var(--ink-muted); + background: var(--paper-deep); + padding: 28px 18px; + text-align: center; + cursor: pointer; + transition: all 0.2s ease; + position: relative; +} +.dropzone:hover, +.dropzone.drag-over { + border-color: var(--accent); + background: var(--rule-soft); + transform: translateY(-1px); +} +.dropzone-inner { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + color: var(--ink-soft); +} +.dropzone-title { + font-family: var(--font-display); + font-size: 1.1rem; + font-style: italic; + font-weight: 400; +} +.dropzone-title em { + font-style: normal; + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--accent); +} +.dropzone-hint { + font-family: var(--font-mono); + font-size: 0.65rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.upload-progress { margin-top: 14px; } +.progress-label { + display: flex; + justify-content: space-between; + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--ink-muted); + margin-bottom: 6px; +} +.progress-track { + height: 4px; + background: var(--rule-soft); + overflow: hidden; +} +.progress-bar { + height: 100%; + width: 0%; + background: var(--accent); + transition: width 0.2s ease; +} + +.archive-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 14px; +} +.archive-head .section-heading { + margin: 0; + border: none; + padding: 0; +} + +.icon-btn { + background: transparent; + border: 1px solid var(--rule); + padding: 5px 8px; + cursor: pointer; + color: var(--ink-soft); + transition: all 0.15s ease; + display: flex; + align-items: center; + justify-content: center; +} +.icon-btn:hover { + border-color: var(--ink); + color: var(--accent); +} + +.pst-list { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 8px; } + +.pst-item { + padding: 14px 14px; + border: 1px solid var(--rule); + background: var(--paper); + cursor: pointer; + transition: all 0.15s ease; + position: relative; +} +.pst-item:hover { + border-color: var(--ink); + transform: translateX(2px); +} +.pst-item.active { + border-color: var(--ink); + background: var(--ink); + color: var(--paper); + box-shadow: 4px 4px 0 var(--accent); +} +.pst-item.active .pst-meta { color: color-mix(in srgb, var(--paper) 65%, transparent); } + +.pst-name { + font-size: 1rem; + font-weight: 500; + letter-spacing: -0.01em; + margin: 0 0 6px; + display: flex; + justify-content: space-between; + align-items: baseline; + gap: 8px; +} +.pst-name-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} +.pst-menu { + background: none; + border: none; + color: inherit; + opacity: 0.4; + cursor: pointer; + font-size: 1.1rem; + line-height: 1; + padding: 0 4px; + letter-spacing: 0.1em; + transition: opacity 0.15s, color 0.15s; +} +.pst-menu:hover { opacity: 1; color: var(--accent); } +.pst-item.active .pst-menu:hover { color: var(--accent-soft); } + +.pst-meta { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.08em; + color: var(--ink-muted); + display: flex; + justify-content: space-between; + gap: 8px; +} + +.pst-status { + text-transform: uppercase; + letter-spacing: 0.12em; +} +.pst-status.indexing { color: var(--warn); } +.pst-status.ready { color: var(--ok); } +.pst-status.failed { color: var(--accent); } +.pst-item.active .pst-status.indexing { color: color-mix(in srgb, var(--warn) 70%, var(--paper) 30%); } +.pst-item.active .pst-status.ready { color: color-mix(in srgb, var(--ok) 70%, var(--paper) 30%); } +.pst-item.active .pst-status.failed { color: var(--accent-soft); } + +.empty-state { + color: var(--ink-muted); + font-style: italic; + text-align: center; + padding: 20px 0; + font-size: 0.9rem; +} + +/* ============================================================ + FOLDER GROUPS IN SIDEBAR + ============================================================ */ +.archive-actions { + display: flex; + gap: 4px; +} + +.folder-group { + list-style: none; + padding: 0; + margin: 0; +} + +.folder-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 4px; + margin: 14px 0 6px; + cursor: pointer; + user-select: none; + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--ink-soft); + border-bottom: 1px solid var(--rule); + transition: color 0.15s; +} +.folder-group:first-child .folder-header { margin-top: 4px; } + +.folder-header:hover { color: var(--accent); } + +.folder-header-left { + display: flex; + align-items: center; + gap: 6px; + overflow: hidden; +} + +.folder-caret { + display: inline-block; + width: 8px; + transition: transform 0.2s; + color: var(--ink-muted); +} +.folder-group.collapsed .folder-caret { transform: rotate(-90deg); } + +.folder-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.folder-count { + color: var(--ink-muted); + font-size: 0.65rem; + margin-left: 4px; +} + +.folder-menu-btn { + background: none; + border: none; + color: var(--ink-muted); + cursor: pointer; + padding: 2px 6px; + font-size: 0.9rem; + line-height: 1; + opacity: 0; + transition: opacity 0.15s, color 0.15s; +} +.folder-header:hover .folder-menu-btn { opacity: 1; } +.folder-menu-btn:hover { color: var(--accent); } + +.folder-items { + display: flex; + flex-direction: column; + gap: 8px; + overflow: hidden; + transition: max-height 0.25s ease-out; + max-height: 2000px; +} +.folder-group.collapsed .folder-items { + max-height: 0; +} + +.folder-empty { + font-style: italic; + font-size: 0.8rem; + color: var(--ink-muted); + padding: 6px 4px; +} + +/* Context menu for folder/PST actions */ +.context-menu { + position: fixed; + background: var(--paper); + border: 1px solid var(--ink); + box-shadow: 4px 4px 0 var(--rule); + z-index: 500; + min-width: 160px; + padding: 4px 0; +} +.context-menu button { + display: block; + width: 100%; + text-align: left; + background: none; + border: none; + padding: 8px 14px; + font-family: var(--font-display); + font-size: 0.9rem; + color: var(--ink); + cursor: pointer; +} +.context-menu button:hover { + background: var(--paper-deep); + color: var(--accent); +} +.context-menu button.danger:hover { + color: var(--accent); +} +.context-menu hr { + border: none; + border-top: 1px solid var(--rule-soft); + margin: 4px 0; +} + +/* ============================================================ + MODAL + ============================================================ */ +.modal { + position: fixed; + inset: 0; + background: color-mix(in srgb, var(--ink) 40%, transparent); + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + z-index: 200; + animation: fadeIn 0.2s ease-out; +} + +.modal-card { + background: var(--paper); + border: 1px solid var(--ink); + padding: 36px 36px 30px; + max-width: 420px; + width: 100%; + box-shadow: 8px 8px 0 var(--accent); + position: relative; +} +.modal-card h2 { + margin: 0 0 4px; + font-size: 1.5rem; + font-weight: 500; + letter-spacing: -0.015em; +} +.modal-subtitle { + margin: 0 0 24px; + font-family: var(--font-mono); + font-size: 0.75rem; + color: var(--ink-muted); + letter-spacing: 0.06em; +} +.modal-close { + position: absolute; + top: 10px; + right: 14px; + background: none; + border: none; + font-size: 1.8rem; + line-height: 1; + cursor: pointer; + color: var(--ink-muted); + transition: color 0.15s, transform 0.15s; +} +.modal-close:hover { + color: var(--accent); + transform: rotate(90deg); +} + +.auth-label select { + font-family: var(--font-display); + font-size: 1rem; + padding: 10px 12px; + border: 1px solid var(--ink); + background: var(--paper-deep); + color: var(--ink); + outline: none; + cursor: pointer; +} +.auth-label select:focus { + background: var(--paper); + box-shadow: 3px 3px 0 var(--accent); +} + +/* ============================================================ + READER + ============================================================ */ +.reader { + display: flex; + flex-direction: column; + min-height: 0; + min-width: 0; +} + +.reader-head { + padding: 28px 40px 20px; + border-bottom: 1px solid var(--rule); + background: + linear-gradient(180deg, rgba(232, 225, 211, 0.3), transparent); +} + +.reader-title h2 { + margin: 0; + font-size: 2rem; + font-weight: 500; + letter-spacing: -0.02em; + line-height: 1.1; +} + +.reader-meta { + margin: 6px 0 20px; + font-family: var(--font-mono); + font-size: 0.72rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.search-wrap { + display: grid; + grid-template-columns: 1fr 200px; + gap: 10px; +} + +#searchInput, #folderFilter { + font-family: var(--font-display); + font-size: 1rem; + padding: 10px 14px; + border: 1px solid var(--ink); + background: var(--paper); + color: var(--ink); + outline: none; + transition: box-shadow 0.15s; +} +#searchInput:focus, #folderFilter:focus { + box-shadow: 3px 3px 0 var(--accent); +} +#searchInput:disabled, #folderFilter:disabled { + border-color: var(--rule); + color: var(--ink-muted); + background: var(--paper-deep); + cursor: not-allowed; +} +#searchInput::placeholder { + color: var(--ink-muted); + font-style: italic; +} +#folderFilter { cursor: pointer; font-family: var(--font-mono); font-size: 0.8rem; } + +.reader-body { + flex: 1; + overflow-y: auto; + padding: 20px 40px; + position: relative; +} + +.reader-placeholder { + text-align: center; + padding: 80px 20px; + color: var(--ink-muted); +} +.placeholder-ornament { + font-size: 2rem; + color: var(--accent); + margin-bottom: 16px; +} +.reader-placeholder p { + font-style: italic; + font-size: 1.1rem; + margin: 0; +} + +/* ============================================================ + MESSAGE LIST + ============================================================ */ +.message-list { + display: flex; + flex-direction: column; +} + +.message-row { + padding: 16px 0; + border-bottom: 1px solid var(--rule-soft); + cursor: pointer; + transition: all 0.12s ease; + display: grid; + grid-template-columns: 180px 1fr 140px; + gap: 20px; + align-items: baseline; +} +.message-row:hover { + background: color-mix(in srgb, var(--paper-deep) 70%, transparent); + padding-left: 8px; + padding-right: 8px; +} + +.mr-sender { + font-weight: 500; + color: var(--ink); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.95rem; +} + +.mr-main { + min-width: 0; + overflow: hidden; +} +.mr-subject { + font-size: 1.05rem; + font-weight: 500; + margin: 0 0 4px; + color: var(--ink); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.mr-snippet { + font-size: 0.85rem; + color: var(--ink-muted); + line-height: 1.5; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; +} +.mr-snippet mark { + background: var(--highlight); + color: var(--ink); + padding: 0 2px; + font-weight: 500; +} + +.mr-meta { + text-align: right; + font-family: var(--font-mono); + font-size: 0.7rem; + color: var(--ink-muted); + letter-spacing: 0.04em; +} +.mr-folder { + margin-top: 4px; + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--accent); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mr-attached { + display: inline-block; + margin-left: 4px; + color: var(--accent); +} + +/* ============================================================ + MESSAGE VIEW + ============================================================ */ +.message-view { + position: absolute; + inset: 20px 40px; + background: var(--paper); + border: 1px solid var(--ink); + box-shadow: 8px 8px 0 var(--rule); + overflow-y: auto; + padding: 40px 50px; + animation: slideIn 0.25s ease-out; +} + +@keyframes slideIn { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} + +.close-btn { + position: absolute; + top: 14px; + right: 18px; + background: none; + border: none; + font-size: 2rem; + line-height: 1; + cursor: pointer; + color: var(--ink-muted); + transition: color 0.15s, transform 0.15s; +} +.close-btn:hover { + color: var(--accent); + transform: rotate(90deg); +} + +.message-header { + border-bottom: 2px solid var(--ink); + padding-bottom: 20px; + margin-bottom: 24px; +} + +.message-subject { + font-size: 1.75rem; + font-weight: 500; + line-height: 1.2; + letter-spacing: -0.015em; + margin-bottom: 16px; +} + +.message-meta { + display: grid; + grid-template-columns: 80px 1fr; + gap: 4px 20px; + margin: 0; +} +.message-meta dt { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--ink-muted); + padding-top: 2px; +} +.message-meta dd { + margin: 0; + font-size: 0.95rem; + color: var(--ink-soft); + word-break: break-word; +} + +.message-body { + font-family: var(--font-display); + font-size: 1rem; + line-height: 1.7; + color: var(--ink); + white-space: pre-wrap; + word-wrap: break-word; + max-width: 70ch; +} + +/* ============================================================ + PAGINATOR + ============================================================ */ +.paginator { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 40px; + border-top: 1px solid var(--rule); + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--ink-soft); + background: var(--paper-deep); +} +.paginator button { + background: none; + border: 1px solid var(--ink); + padding: 6px 14px; + cursor: pointer; + font-family: inherit; + font-size: inherit; + color: var(--ink); + transition: all 0.15s; +} +.paginator button:hover:not(:disabled) { + background: var(--ink); + color: var(--paper); +} +.paginator button:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +/* ============================================================ + TOAST + ============================================================ */ +.toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: var(--ink); + color: var(--paper); + padding: 12px 20px; + font-family: var(--font-mono); + font-size: 0.8rem; + letter-spacing: 0.06em; + border-left: 3px solid var(--accent); + box-shadow: var(--shadow); + z-index: 1000; + animation: toastSlide 0.3s ease-out; +} +@keyframes toastSlide { + from { opacity: 0; transform: translate(-50%, 20px); } + to { opacity: 1; transform: translate(-50%, 0); } +} + +.hidden { display: none !important; } + +/* ============================================================ + AUTH SCREENS (login + setup) + ============================================================ */ +.auth-screen { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + background: + radial-gradient(circle at 30% 20%, color-mix(in srgb, var(--accent) 12%, transparent), transparent 45%), + radial-gradient(circle at 70% 80%, color-mix(in srgb, var(--ink) 10%, transparent), transparent 55%), + var(--paper); + z-index: 100; + animation: fadeIn 0.3s ease-out; +} +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.auth-card { + width: 100%; + max-width: 440px; + background: var(--paper); + border: 1px solid var(--ink); + padding: 44px 40px 36px; + box-shadow: 8px 8px 0 var(--rule); + position: relative; +} +.auth-card::before { + content: ""; + position: absolute; + top: -6px; + left: -6px; + right: 6px; + bottom: 6px; + border: 1px solid var(--ink); + pointer-events: none; + z-index: -1; + background: var(--paper-deep); +} + +.auth-brand { + text-align: center; + margin-bottom: 28px; + padding-bottom: 20px; + border-bottom: 1px solid var(--rule); +} +.auth-brand svg { color: var(--ink); margin: 0 auto 12px; display: block; } +.auth-brand h1 { + margin: 0; + font-size: 2rem; + font-weight: 500; + letter-spacing: -0.02em; + line-height: 1; +} +.auth-tagline { + margin: 8px 0 0; + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--ink-muted); +} + +.auth-form { display: flex; flex-direction: column; gap: 16px; } + +.auth-intro { + margin: 0 0 6px; + font-size: 0.92rem; + color: var(--ink-soft); + line-height: 1.5; + font-style: italic; +} + +.auth-label { + display: flex; + flex-direction: column; + gap: 6px; +} +.auth-label > span { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--ink-soft); +} +.auth-label small { + font-family: var(--font-mono); + font-size: 0.66rem; + color: var(--ink-muted); + letter-spacing: 0.04em; +} +.auth-label input { + font-family: var(--font-display); + font-size: 1rem; + padding: 10px 12px; + border: 1px solid var(--ink); + background: var(--paper-deep); + color: var(--ink); + outline: none; + transition: box-shadow 0.15s, background 0.15s; +} +.auth-label input:focus { + background: var(--paper); + box-shadow: 3px 3px 0 var(--accent); +} + +.auth-btn { + margin-top: 8px; + padding: 12px 18px; + font-family: var(--font-display); + font-size: 1.05rem; + font-weight: 500; + letter-spacing: -0.01em; + background: var(--ink); + color: var(--paper); + border: 1px solid var(--ink); + cursor: pointer; + transition: all 0.15s; +} +.auth-btn:hover { + background: var(--accent); + border-color: var(--accent); + transform: translate(-1px, -1px); + box-shadow: 3px 3px 0 var(--ink); +} + +.auth-error { + padding: 10px 14px; + background: color-mix(in srgb, var(--accent) 10%, transparent); + border-left: 3px solid var(--accent); + color: var(--accent); + font-size: 0.88rem; + font-family: var(--font-mono); + letter-spacing: 0.02em; +} + +/* ============================================================ + USER CHIP in masthead + ============================================================ */ +.user-chip { + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ink-soft); + padding: 4px 10px; + border: 1px solid var(--rule); + background: var(--paper-deep); + margin-left: 12px; +} + +.logout-btn { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--ink-soft); + background: transparent; + border: 1px solid var(--rule); + padding: 5px 10px; + cursor: pointer; + transition: all 0.15s; + margin-left: 8px; +} +.logout-btn:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* ADMINISTRATION button — shown next to the username for admin users only */ +.admin-nav-btn { + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.14em; + text-transform: uppercase; + color: var(--paper); + background: var(--accent); + border: 1px solid var(--accent); + padding: 5px 12px; + cursor: pointer; + transition: all 0.15s; + margin-left: 8px; + font-weight: 500; +} +.admin-nav-btn:hover { + background: var(--accent-soft); + border-color: var(--accent-soft); + transform: translateY(-1px); +} + +/* ============================================================ + SITE FOOTER (shown on the app view, not on login/setup) + ============================================================ */ +#mainApp:not(.hidden) { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.site-footer { + flex-shrink: 0; + padding: 14px 40px; + border-top: 1px solid var(--ink); + font-family: var(--font-mono); + font-size: 0.68rem; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--ink-muted); + text-align: center; + background: var(--paper-deep); +} +@media (max-width: 900px) { + .site-footer { padding: 12px 20px; font-size: 0.62rem; } +} + +/* ============================================================ + THEME TOGGLE + ============================================================ */ +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: 1px solid var(--rule); + background: var(--paper-deep); + color: var(--ink-soft); + cursor: pointer; + padding: 0; + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} +.theme-toggle:hover { + border-color: var(--accent); + color: var(--accent); + transform: translateY(-1px); +} +.theme-toggle svg { + width: 16px; + height: 16px; + transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} +.theme-toggle:hover svg { + transform: rotate(25deg); +} +:root[data-theme="dark"] .theme-toggle:hover svg { + transform: rotate(-25deg); +} + +/* Icon visibility — show sun in light mode, moon in dark mode */ +.theme-toggle .icon-sun { display: inline-block; } +.theme-toggle .icon-moon { display: none; } +:root[data-theme="dark"] .theme-toggle .icon-sun { display: none; } +:root[data-theme="dark"] .theme-toggle .icon-moon { display: inline-block; } + +/* Floating variant shown on auth/setup screens */ +.theme-toggle-float { + position: fixed; + top: 20px; + right: 20px; + z-index: 200; +} +/* Hide the floating toggle once the main app is visible (in-masthead toggle takes over) */ +body:has(#mainApp:not(.hidden)) .theme-toggle-float { display: none; } + +/* ============================================================ + SCROLLBARS + ============================================================ */ +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: var(--rule); + border: 2px solid var(--paper); +} +::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); } + +/* ============================================================ + RESPONSIVE + ============================================================ */ +@media (max-width: 900px) { + .workspace { grid-template-columns: 1fr; } + .sidebar { border-right: none; border-bottom: 1px solid var(--rule); max-height: 50vh; } + .masthead { padding: 20px 24px 14px; flex-wrap: wrap; gap: 16px; } + .reader-head, .reader-body, .paginator { padding-left: 24px; padding-right: 24px; } + .search-wrap { grid-template-columns: 1fr; } + .message-row { grid-template-columns: 1fr; gap: 6px; } + .mr-meta { text-align: left; } + .message-view { inset: 12px; padding: 24px 20px; } + .message-meta { grid-template-columns: 60px 1fr; } +}