Skip to content

feat(legal): publish privacy, terms and legal notice pages - #28

Merged
thoda-dev merged 1 commit into
masterfrom
legal-documents
Aug 31, 2026
Merged

feat(legal): publish privacy, terms and legal notice pages#28
thoda-dev merged 1 commit into
masterfrom
legal-documents

Conversation

@thoda-dev

Copy link
Copy Markdown
Owner

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/legal and 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_documents table, not in a mounted file. The app service has no volume today and uploads themselves are bytea in Postgres; adding a bind mount for one Markdown file would mean a compose change, an install.sh change, 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, drops on* handlers and limits link protocols 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 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 typecheck and pnpm test pass
  • Schema change? A migration is committed alongside it (pnpm db:generate) — 0004_naive_night_thrasher.sql, one new table
  • Touches crypto, authorization, or the paste read counter? Say so here — those get a closer read

Nothing touches crypto or the read counter. Four new handlers live under server/api/: the three admin ones call the existing requireSuperAdminSession, and GET /api/legal/[slug] is deliberately public. None of them decides authorization themselves, but they are new endpoints and worth a look. GET /api/settings/public gains 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 of mail-locale.ts into a shared module now that mail is not its only consumer. MAIL_LOCALES and 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.

  • No AI tool was used
  • An AI tool was used — which one, and roughly how much of the change: Claude Code, most of the change. It wrote the schema, handlers, components, templates and tests from a spec discussed decision by decision; the storage choice, the parser lockdown and the template-fetching behaviour were argued out before any code. I reviewed every line. The interactive path through the fill-in dialog was exercised by hand rather than by tests, so that is where to look hardest.

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.
Copilot AI lite review requested due to automatic review settings August 31, 2026 15:03
@thoda-dev
thoda-dev merged commit a610064 into master Aug 31, 2026
6 checks passed
@thoda-dev
thoda-dev deleted the legal-documents branch August 31, 2026 15:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_documents table plus server utilities and endpoints to publish/fetch legal documents and bundled templates.
  • Adds admin UI at /admin/legal with 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'
Comment thread package.json
},
"devDependencies": {
"@nuxt/cli": "4.0.0-alpha.0",
"@nuxt/cli": "alpha",
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Legal pages: let an operator publish a privacy policy, terms and a legal notice

2 participants