feat(legal): publish privacy, terms and legal notice pages - #28
Merged
Conversation
Three documents — privacy policy, terms of use, legal notice — written in Markdown per language, published from the admin dashboard and served at /legal/<slug>. They live in the database like every other setting: no volume to mount, nothing extra to back up. A document nobody wrote has no page and no footer link, and a read falls back across languages so a link always resolves. Rendering goes through Comark with its default plugins off: no raw HTML, no component syntax, no free-form attributes, and link protocols limited to http, https and mailto. In an app where the browser holds the only decryption key, a script tag on any page is a leaked key, not a defaced page. Templates for all three ship inside the image in English and French. Inserting one opens a form built from the template's own placeholders and fills every selected language at once — durations are a number and a unit written in each language's words, and paste retention, session lifetime and ban duration arrive prefilled from the instance's configuration. A blank field keeps its placeholder; a field switched off takes its line with it. The editor shows Markdown beside a live preview using the same parser as the public page, and one Save stores every language that has unsaved text. The footer these pages introduce pushed centred pages past 100vh, so the header moves from fixed to in-flow and the admin screens share one max width. Provisional: the responsive pass will revisit it.
There was a problem hiding this comment.
🟡 Changes recommended
It introduces at least one non-reproducible dependency spec and a confirmed security/config mismatch around rendered <img> support that should be resolved before approval.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds first-class “legal pages” support so instance operators can author/publish a privacy policy, terms of use, and legal notice (per locale) from a super-admin UI, serve them publicly at /legal/<slug>, and surface links in the footer—addressing the compliance gap described in #27.
Changes:
- Introduces a
legal_documentstable plus server utilities and endpoints to publish/fetch legal documents and bundled templates. - Adds admin UI at
/admin/legalwith template fill-in workflow, live preview using the same locked-down Markdown parser as public rendering, and i18n strings/tests. - Updates app layout to include a footer with legal links and adjusts page/container sizing to accommodate the new footer/header flow.
File summaries
| File | Description |
|---|---|
| README.md | Mentions the new legal pages feature in the project feature list. |
| package.json | Updates Nuxt CLI devDependency version spec. |
| apps/docs/nuxt.config.ts | Adds a docs navigation icon for the new legal pages doc. |
| apps/docs/content/2.self-hosting/5.legal-pages.md | Documents how operators publish/manage legal pages. |
| apps/app/tests/locales.test.ts | Adds locale consistency/escaping/unit-key coverage for new legal i18n. |
| apps/app/tests/legal-template.test.ts | Tests placeholder extraction and template fill/drop behavior. |
| apps/app/tests/legal-markdown.test.ts | Tests Markdown parsing hardening (no HTML/script, protocol allowlist, breaks). |
| apps/app/shared/utils/legal-template.ts | Adds shared placeholder/template fill utilities for the admin template dialog. |
| apps/app/shared/utils/legal-markdown.ts | Adds the locked-down Markdown parser used by admin preview and public pages. |
| apps/app/server/utils/mail-locale.ts | Moves locale list to shared server locale module (keeps mail helpers stable). |
| apps/app/server/utils/locales.ts | Centralizes app locale constants/type guards for server usage. |
| apps/app/server/utils/legal.ts | Adds legal slugs, DB accessors, template asset access, and suggested duration helpers. |
| apps/app/server/utils/ip-security.ts | Exposes auto-ban duration helper for suggested durations. |
| apps/app/server/database/schema/legal-document.ts | Adds Drizzle schema for legal_documents. |
| apps/app/server/database/schema/index.ts | Exports the new legal document schema. |
| apps/app/server/database/migrations/meta/0004_snapshot.json | Captures schema snapshot for migration 0004. |
| apps/app/server/database/migrations/meta/_journal.json | Registers migration 0004 in the drizzle journal. |
| apps/app/server/database/migrations/0004_naive_night_thrasher.sql | Creates the enum + legal_documents table. |
| apps/app/server/assets/legal/terms.fr.md | Adds bundled French terms template asset. |
| apps/app/server/assets/legal/terms.en.md | Adds bundled English terms template asset. |
| apps/app/server/assets/legal/privacy.fr.md | Adds bundled French privacy template asset. |
| apps/app/server/assets/legal/privacy.en.md | Adds bundled English privacy template asset. |
| apps/app/server/assets/legal/notice.fr.md | Adds bundled French legal notice template asset. |
| apps/app/server/assets/legal/notice.en.md | Adds bundled English legal notice template asset. |
| apps/app/server/api/settings/public.get.ts | Exposes published legal slugs via public settings for footer rendering. |
| apps/app/server/api/legal/[slug].get.ts | Adds public endpoint to fetch a rendered legal document by slug + locale. |
| apps/app/server/api/admin/legal/template.get.ts | Adds admin endpoint to fetch the bundled template per slug/locale. |
| apps/app/server/api/admin/legal/index.put.ts | Adds admin endpoint to upsert/delete legal documents with audit logging. |
| apps/app/server/api/admin/legal/index.get.ts | Adds admin endpoint to list documents/templates and suggested durations. |
| apps/app/package.json | Adds Comark + bumps dependencies needed for rendering legal Markdown. |
| apps/app/nuxt.config.ts | Registers the Comark Nuxt module. |
| apps/app/i18n/locales/fr.json | Adds admin/legal UI strings + public legal page strings in French. |
| apps/app/i18n/locales/en.json | Adds admin/legal UI strings + public legal page strings in English. |
| apps/app/app/pages/setup.vue | Adjusts page container sizing for new app layout flow. |
| apps/app/app/pages/register.vue | Adjusts page container sizing for new app layout flow. |
| apps/app/app/pages/p/[id].vue | Adjusts page container sizing for new app layout flow. |
| apps/app/app/pages/login.vue | Adjusts page container sizing for new app layout flow. |
| apps/app/app/pages/legal/[slug].vue | Adds the public legal document page UI. |
| apps/app/app/pages/index.vue | Adjusts page container sizing for new app layout flow. |
| apps/app/app/pages/dashboard.vue | Adjusts top padding for new in-flow header layout. |
| apps/app/app/pages/admin/users.vue | Expands admin page width / adjusts layout spacing. |
| apps/app/app/pages/admin/storage.vue | Expands admin page width / adjusts layout spacing. |
| apps/app/app/pages/admin/settings.vue | Expands admin page width / adjusts layout spacing. |
| apps/app/app/pages/admin/legal.vue | Adds the super-admin legal documents editor + preview + template workflow. |
| apps/app/app/pages/admin/invitations.vue | Expands admin page width / adjusts layout spacing. |
| apps/app/app/pages/admin/banned-ips.vue | Expands admin page width / adjusts layout spacing. |
| apps/app/app/pages/admin/allowed-ips.vue | Expands admin page width / adjusts layout spacing. |
| apps/app/app/pages/account.vue | Adjusts top padding for new in-flow header layout. |
| apps/app/app/middleware/require-2fa.global.ts | Exempts /legal/* routes from the 2FA redirect UX middleware. |
| apps/app/app/composables/usePublicSettings.ts | Adds legalDocuments to the public settings shape for footer rendering. |
| apps/app/app/components/LegalTemplateDialog.vue | Adds the template fill-in modal (placeholders → fields, multi-locale fill). |
| apps/app/app/components/LegalDocument.vue | Adds the document renderer wrapper for Comark output. |
| apps/app/app/components/AppFooter.vue | Adds footer links for published legal documents. |
| apps/app/app/components/AdminNav.vue | Adds the Legal admin nav link (super-admin only). |
| apps/app/app/app.vue | Moves header into normal flow and adds footer to the global app layout. |
Review details
- Files reviewed: 55/56 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+12
to
+13
| 'table', 'thead', 'tbody', 'tr', 'th', 'td', | ||
| 'a', 'img' |
| }, | ||
| "devDependencies": { | ||
| "@nuxt/cli": "4.0.0-alpha.0", | ||
| "@nuxt/cli": "alpha", |
This was referenced Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Closes #27
Adds three legal documents — privacy policy, terms of use, legal notice — written in Markdown per language, published from a new super-admin screen at
/admin/legaland served at/legal/<slug>. Templates for all three ship inside the image in English and French, with a form that fills their placeholders for every selected language at once.Why
Running an instance makes its operator the data controller for it, and the software gave them nowhere to say so. Issue #27 has the detail; the short version is that an instance processes personal data around a paste — account data, IP addresses in sessions and ban lists, recipient addresses on shared pastes — and the person who receives a shared link never visited the site at all.
Three decisions worth reviewing rather than taking on trust:
Storage. Documents live in a
legal_documentstable, not in a mounted file. The app service has no volume today and uploads themselves arebyteain Postgres; adding a bind mount for one Markdown file would mean a compose change, aninstall.shchange, a file missing from backups, and nothing at all on a host without persistent disk.Rendering. Comark runs with
registerDefaultPlugins: false, so raw HTML, component syntax and free-form attributes are all off, and its security plugin keeps the tree to the tags plain Markdown produces, dropson*handlers and limits link protocols tohttp,httpsandmailto. In an app where the browser holds the only decryption key, a script tag on any page is a leaked key rather than a defaced page. Public pages are parsed server-side; the admin preview uses the same parser so it cannot show what the served page would drop.The template button fetches nothing. The starting text is a Nitro server asset bundled into the image, so an instance behind a closed network can still publish a policy, and the template always matches the version actually running instead of whatever is on
master.Absence is the meaningful state throughout: a document nobody wrote has no page and no footer link, so an instance that ignores this feature looks exactly as it did before.
Checks
pnpm lint,pnpm typecheckandpnpm testpasspnpm db:generate) —0004_naive_night_thrasher.sql, one new tableNothing touches crypto or the read counter. Four new handlers live under
server/api/: the three admin ones call the existingrequireSuperAdminSession, andGET /api/legal/[slug]is deliberately public. None of them decides authorization themselves, but they are new endpoints and worth a look.GET /api/settings/publicgains one field,legalDocuments, listing published slugs — no more revealing than the footer it drives.Two changes outside the feature:
/legal/*is exempt from the two-factor redirect, since pinning somebody away from the privacy policy is an odd way to enforce 2FA, and the locale list moves out ofmail-locale.tsinto a shared module now that mail is not its only consumer.MAIL_LOCALESand its helpers keep their signatures.The footer these pages introduce pushed every centred page past 100vh, so the header moves from fixed to in-flow — it was floating over content scrolling beneath it on short viewports — and the admin screens share one max width. That part is provisional and the planned responsive pass will revisit it.
AI assistance
See CONTRIBUTING.md. Disclosure is not held against you: it tells
the reviewer where to look hardest.