Skip to content
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ Versions follow [Semantic Versioning](https://semver.org/).

## [Unreleased]

### Fixed — self-hosting and security docs: systemd unit, licences and release facts

The self-hosting, installation, development, security and licensing docs had
drifted from the code they describe. The systemd unit was missing `/usr/bin`
on `PATH` (ffmpeg/Ghostscript/LibreOffice invisible to a non-Docker install),
never loaded `.env` into the process environment (`DATABASE_URL`,
`FORWARDED_ALLOW_IPS` and `FILEMORPH_IMAGE_MAX_MEGAPIXELS` are read directly
from `os.environ`, not through the app's own `.env` parsing), bound
`0.0.0.0` instead of `127.0.0.1`, and ran multiple workers despite the rate
limiter and concurrency caps being per-process. Account deletion, the Stripe
webhook table and the audit-log hash paragraph described a pre-launch state
(Stripe-touched accounts refused with 409) instead of the shipped
tax-retained deletion path. `third-party-licenses.md` was missing
Ghostscript, LibreOffice, `pillow-avif-plugin` and the vendored Chart.js /
Tailwind assets, still listed `httpx` as a runtime dependency (it's dev-only),
and pointed at a scan/SBOM that predates the lockfile-parity and
SBOM-hardening work. `patch-policy.md` claimed every merge is tagged and a
release lands every 1–4 weeks; it now describes the actual two-track model
(continuous `latest`/`sha-*` builds on every merge, a `vX.Y.Z`/`X.Y.Z` tag at
the maintainer's discretion — one so far, `v1.1.0`). Smaller fixes:
`/ready` doesn't check ffmpeg (DB + tempdir only), `APP_PORT` is read only by
`run.py`, `python3.11` is no longer an installable apt package on current
Ubuntu/Debian, a Docker UID/GID mismatch on `./data` needs a `chown` not a
`chmod`, the email-setup guide's "no SMTP" behaviour is `200` + a log line
(not `503`), and the pentest report's Tailwind-CDN / inline-script claims
are corrected in place rather than silently rewritten.

### Added — the Docker images carry a signed SBOM attestation

`docker.yml` now attests the CycloneDX SBOM to each image it pushes — slim and
Expand Down
99 changes: 79 additions & 20 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,42 @@ filemorph/
│ │ ├── convert.py # POST /api/v1/convert
│ │ ├── compress.py # POST /api/v1/compress
│ │ ├── formats.py # GET /api/v1/formats
│ │ └── health.py # GET /api/v1/health
│ │ ├── health.py # GET /api/v1/health, /ready
│ │ ├── auth.py # Cloud Edition: register / login / account
│ │ ├── keys.py # Cloud Edition: dashboard API-key CRUD
│ │ ├── billing.py # Cloud Edition: Stripe checkout + webhook
│ │ └── cockpit.py # Cloud Edition: admin routes
│ ├── core/
│ │ ├── config.py # Settings loaded from .env via pydantic-settings
│ │ └── security.py # API key generation, hashing, validation
│ │ ├── security.py # API key generation, hashing, validation
│ │ ├── rate_limit.py # slowapi limiter + failed-key budget
│ │ ├── quotas.py # Per-tier size / concurrency / output caps
│ │ └── audit.py # Tamper-evident audit-log hash chain
│ ├── converters/
│ │ ├── base.py # AbstractConverter base class
│ │ ├── base.py # BaseConverter base class
│ │ ├── registry.py # Converter registry (@register decorator)
│ │ ├── image.py # Image conversions (Pillow + pillow-heif)
│ │ ├── image.py # Image conversions (Pillow + pillow-heif + pillow-avif-plugin)
│ │ ├── document.py # Document conversions (docx, pdf, txt, md)
│ │ ├── video.py # Video conversions (ffmpeg-python)
│ │ ├── audio.py # Audio conversions (ffmpeg-python)
│ │ └── spreadsheet.py # Spreadsheet conversions (openpyxl, csv, json)
│ ├── compressors/
│ │ ├── image.py # Image quality compression (Pillow)
│ │ └── video.py # Video CRF compression (ffmpeg)
│ │ ├── image.py # Image quality / target-size compression (Pillow)
│ │ ├── video.py # Video CRF compression (ffmpeg)
│ │ └── pdf.py # PDF compression
│ ├── db/ # SQLAlchemy models + async engine (Cloud Edition)
│ ├── ee/ # Commercial-licensed add-ons (PII redaction) — inert by default
│ ├── models/
│ │ └── schemas.py # Pydantic response schemas
│ ├── static/ # CSS and JavaScript
│ ├── static/ # CSS, JavaScript and vendored assets (Chart.js)
│ └── templates/ # Jinja2 HTML templates
├── alembic/ # Cloud Edition schema migrations
├── tests/
├── scripts/
│ ├── generate_api_key.py # CLI key generator
│ └── first_run.py # Called by Docker entrypoint on first start
│ ├── promote_admin.py # Promote a registered user to the admin role
│ ├── first_run.py # Called by Docker entrypoint on first start
│ └── build-tailwind.sh # Rebuilds the self-hosted Tailwind bundle
├── data/
│ └── api_keys.json # Hashed API keys (gitignored)
├── run.py # Entry point for direct Python runs
Expand All @@ -48,7 +61,11 @@ filemorph/
├── start.bat # Windows launcher: Docker mode
├── start.sh # Linux/macOS launcher: Docker mode
├── entrypoint.sh # Docker container entrypoint (first-run key setup)
└── docker-compose.yml
├── docker-compose.yml # Community Edition (default)
├── docker-compose.cloud.yml # Cloud Edition overlay (Postgres, JWT, billing)
├── docker-compose.office.yml # High-fidelity docx→pdf overlay (LibreOffice)
├── requirements.txt # Direct dependencies (source of truth for versions)
└── requirements.lock # Hash-pinned lockfile — what the image actually installs
```

---
Expand All @@ -67,13 +84,21 @@ cd FileMorph
first run, and starts uvicorn with `--reload`. Code changes are picked up automatically
without restarting the server.

`dev.ps1` installs only `requirements.txt` (the runtime dependencies). To run
tests or lint locally, also install the dev tools it doesn't cover
(`pytest`, `ruff`, `pip-audit`, …):

```powershell
.venv\Scripts\pip.exe install -r requirements-dev.txt
```

### Linux / macOS

```bash
git clone https://github.com/MrChengLen/FileMorph.git
cd FileMorph

python3.11 -m venv .venv
python3 -m venv .venv # Python 3.11 or newer
source .venv/bin/activate

pip install -r requirements-dev.txt
Expand All @@ -85,7 +110,7 @@ uvicorn app.main:app --reload

### Live reload

With `--reload`, uvicorn watches `Z:\Python\projects\filemorph` for file changes and
With `--reload`, uvicorn watches the project directory for file changes and
restarts the server process automatically. No manual restart needed when editing Python files.

---
Expand All @@ -101,7 +126,8 @@ Run a single test file:
pytest tests/test_convert_image.py -v
```

Run with coverage:
Run with coverage (optional — `pytest-cov` isn't in `requirements-dev.txt`,
install it separately: `pip install pytest-cov`):
```bash
pytest tests/ --cov=app --cov-report=term-missing
```
Expand Down Expand Up @@ -248,9 +274,11 @@ def test_epub_to_txt(client, auth_headers, tmp_path):
assert len(res.content) > 0
```

### Step 6 — Update format documentation
### Step 6 — Update the format lists

Add the new format to [docs/formats.md](formats.md).
Formats are also listed by hand in several places — [docs/formats.md](formats.md) is one —
and tests compare them with the registry, so the build fails until they agree. The full
list, with the test that pins each place, is under "Parity places" below.

---

Expand All @@ -268,6 +296,33 @@ def compress_image(input_path: Path, output_path: Path, quality: int = 85) -> Pa
Add the new format to the `_SUPPORTED_FORMATS` list in the relevant compressor file,
and import + call it from `app/api/routes/compress.py`.

**Parity places to update in the same PR**, for either a new converter or a new
compressor format — each is pinned by a test, so a missed one fails the build:

- `docs/formats.md` (the From → To tables and the Audio/Video lists), the README
(the drop-zone mockup and the "Supported Formats" table), the homepage FAQ
answer "Which file formats can I convert?" (EN + DE), the "FileMorph converts …"
sentence in `/llms.txt`, and the "Convert …" entries of the JSON-LD `featureList`
(`app/core/jsonld.py`) — `tests/test_format_lists_match_registry.py` compares
each with the registry.
- The homepage drop-zone captions `#supported-convert` and `#supported-compress`
(`app/templates/partials/convert_tool.html`, EN + DE) —
`tests/test_homepage_drop_zone_modes.py` compares them with `/api/v1/formats`.
- `_HOMEPAGE_ADVERTISED` in `tests/test_format_registry.py` — the test's own list of
the formats the homepage names. Add the format there; the test only checks that
every listed format is registered, so a missing entry goes unnoticed.
- `_FORMAT_CATEGORY` in `app/api/routes/pages.py` — `tests/test_formats_categories.py`
fails for a source format without a category (it would land in the "Other"
bucket on `/formats`).
- If the format supports exact-size compression, `TARGET_SIZE_FORMATS` in
`app/compressors/image.py` **and** the matching `TARGET_SIZE_FORMATS` array
in `app/static/js/app.js` — `tests/test_target_size_formats_parity.py`
fails the build if the two disagree, and also pins the hand-written claims about
which formats hit an exact target (homepage FAQ, `/formats`, `/llms.txt`, the
OpenAPI form-field docs, the `/tools` card); the `/compress` page copy is pinned
by `tests/test_compress_page.py`.
- A test per new format/pair (Step 5 above / the equivalent for compressors).

---

## API key internals
Expand All @@ -284,21 +339,25 @@ All logic is in `app/core/security.py`.

## Environment variables reference

Defined in `app/core/config.py` using pydantic-settings:
Defined in `app/core/config.py` using pydantic-settings — a representative
slice (the real class has ~40 fields: JWT, Stripe, SMTP, audit-log,
concurrency, AI-redaction and office-engine settings besides these):

```python
class Settings(BaseSettings):
app_host: str = "0.0.0.0"
app_port: int = 8000
app_debug: bool = False
app_version: str = "1.0.0"
api_keys_file: str = "data/api_keys.json"
app_version: str = "1.1.0"
api_keys_file: str = "" # resolved to data/api_keys.json if left empty
max_upload_size_mb: int = 100
cors_origins: str = "*"
cors_origins: str = "http://localhost:8000"
```

All settings can be overridden via environment variables or `.env` (uppercase, same names):
`APP_HOST`, `APP_PORT`, `APP_DEBUG`, `API_KEYS_FILE`, `MAX_UPLOAD_SIZE_MB`, `CORS_ORIGINS`
All settings can be overridden via environment variables or `.env`
(uppercase, same names). [`.env.example`](../.env.example) is the
source of truth for the full list — every variable there carries a
one-line description; this section only shows the shape.

---

Expand Down
36 changes: 26 additions & 10 deletions docs/email-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,19 @@ is unset (NULL) the operator default `LANG_DEFAULT` applies. The dunning mail
fires from a Stripe webhook with no HTTP request to derive a locale from, which
is exactly why the column exists.

If `SMTP_HOST` is empty, every feature above degrades gracefully:

- `/forgot-password` and `/resend-verification` return `503 Service Unavailable`
with a reason string the UI surfaces.
If `SMTP_HOST` is empty, every feature above degrades gracefully — all of
them return their normal success status regardless, and the outbound
send is silently skipped:

- `/forgot-password` and `/resend-verification` still return `200`.
`send_email()` sees `SMTP_HOST` is empty, logs `send_email skipped —
SMTP not configured (to_domain=…, subject=…)` at WARNING and returns
without sending — no exception reaches the route, so there is no
error status to surface. `/forgot-password` is deliberately
enumeration-safe: it always returns the same generic response whether
the address exists, the user is inactive, or the email was skipped,
so the response alone never tells a caller which case occurred — check
the application log to see what actually happened.
- `/register` still creates the user — the verification email is fire-and-forget.
The user can request a fresh link via `/resend-verification` once SMTP is wired.
- `/auth/account` deletes the user even if the confirmation email cannot be sent;
Expand Down Expand Up @@ -168,12 +177,19 @@ curl -X POST https://your-domain.example.com/api/v1/auth/forgot-password \
# Expected: an email arrives at the inbox within seconds.
```

If no mail arrives, check:

1. **Application log** — the sender logs `send_email ok` (success) or
`send_email failed` (failure). The latter is logged at exception level
with full SMTP error details visible only in the server log; the HTTP
response stays generic so the SMTP details never leak to the client.
The request returns `200` either way (see "What needs SMTP" above), so a
successful-looking response does not by itself mean the email was sent —
check the application log for what actually happened. If no mail
arrives, check:

1. **Application log first.** Three possible lines: `send_email skipped
— SMTP not configured` (WARNING — `SMTP_HOST` is empty; the container
may not have picked up an `.env` change, or the overlay/unit file
isn't passing it through), `send_email ok` (success — the message left
for the provider; a delivery problem from here on is provider-side),
or `send_email failed` (the exception is logged server-side with full
SMTP error details; the HTTP response stays generic so those details
never leak to the client).
2. **DNS / SPF / DKIM / DMARC** — for ESPs and mailbox providers, the
sending domain must have valid SPF and DKIM records pointing at the
provider, plus a DMARC policy. Without alignment, Gmail and Outlook
Expand Down
7 changes: 4 additions & 3 deletions docs/incident-response.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ contact is added to the response thread before the advisory goes public.
image is signed and pushed, and the advisory is published. Compliance-
Edition customers on the security mailing list receive the advisory
five working days before public disclosure when feasible.
6. **Post-mortem.** Within 30 days of disclosure the maintainer publishes
a short post-mortem in the project's `runbooks/` (where a runbook
directory exists) or as a follow-up release note. Format below.
6. **Post-mortem.** Within 30 days of disclosure the maintainer records a
short post-mortem in a private incident log and, where the details are
appropriate for a public audience, publishes a follow-up release note.
Format below.

## Post-mortem template

Expand Down
Loading
Loading