diff --git a/README.md b/README.md index 9e8084f..28675be 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,7 @@ without trusting the server with the contents. - Email sharing at creation time, one message with recipients in blind copy - Cloudflare Turnstile, per-IP and per-account rate limiting, automatic probe banning - Account deletion with full cascade (GDPR right to erasure) +- Legal pages — privacy policy, terms, legal notice — written in Markdown from the admin dashboard, with templates included - English and French ## Deploy diff --git a/apps/app/app/app.vue b/apps/app/app/app.vue index 136f315..5a7b77e 100644 --- a/apps/app/app/app.vue +++ b/apps/app/app/app.vue @@ -34,34 +34,39 @@ useHead({ diff --git a/apps/app/app/components/AdminNav.vue b/apps/app/app/components/AdminNav.vue index a76fef9..63aca90 100644 --- a/apps/app/app/components/AdminNav.vue +++ b/apps/app/app/components/AdminNav.vue @@ -18,7 +18,8 @@ const links = computed(() => { { to: '/admin/allowed-ips', icon: 'i-lucide-shield', label: t('admin.allowedIps.title') }, { to: '/admin/banned-ips', icon: 'i-lucide-ban', label: t('admin.bannedIps.title') }, { to: '/admin/users', icon: 'i-lucide-users', label: t('admin.users.title') }, - { to: '/admin/invitations', icon: 'i-lucide-mail-plus', label: t('admin.invitations.title') } + { to: '/admin/invitations', icon: 'i-lucide-mail-plus', label: t('admin.invitations.title') }, + { to: '/admin/legal', icon: 'i-lucide-scale', label: t('admin.legal.title'), superAdminOnly: true } ] return all.filter(link => !link.superAdminOnly || user.value?.role === 'super_admin') }) diff --git a/apps/app/app/components/AppFooter.vue b/apps/app/app/components/AppFooter.vue new file mode 100644 index 0000000..fabcecb --- /dev/null +++ b/apps/app/app/components/AppFooter.vue @@ -0,0 +1,23 @@ + + + diff --git a/apps/app/app/components/LegalDocument.vue b/apps/app/app/components/LegalDocument.vue new file mode 100644 index 0000000..a99dd32 --- /dev/null +++ b/apps/app/app/components/LegalDocument.vue @@ -0,0 +1,160 @@ + + + + + diff --git a/apps/app/app/components/LegalTemplateDialog.vue b/apps/app/app/components/LegalTemplateDialog.vue new file mode 100644 index 0000000..982df6c --- /dev/null +++ b/apps/app/app/components/LegalTemplateDialog.vue @@ -0,0 +1,237 @@ + + + diff --git a/apps/app/app/composables/usePublicSettings.ts b/apps/app/app/composables/usePublicSettings.ts index f931ca0..b17005d 100644 --- a/apps/app/app/composables/usePublicSettings.ts +++ b/apps/app/app/composables/usePublicSettings.ts @@ -1,9 +1,12 @@ +export type LegalSlug = 'privacy' | 'terms' | 'notice' + export interface PublicSettings { publicPasteEnabled: boolean registrationEnabled: boolean require2fa: boolean mailEnabled: boolean maxEmailRecipients: number | null + legalDocuments: LegalSlug[] limits: { anonymous: TierLimits authenticated: TierLimits diff --git a/apps/app/app/middleware/require-2fa.global.ts b/apps/app/app/middleware/require-2fa.global.ts index 719dcc8..f9fa4fb 100644 --- a/apps/app/app/middleware/require-2fa.global.ts +++ b/apps/app/app/middleware/require-2fa.global.ts @@ -1,6 +1,6 @@ // Client half of `app_settings.require_2fa`: a non-enrolled account is pinned to /account until it enrolls. // The server half lives in `requireAdminSession` and `POST /api/pastes` — this is the UX, not the security boundary. -const ALLOWED_ROUTE_PREFIXES = ['account', 'login', 'setup'] +const ALLOWED_ROUTE_PREFIXES = ['account', 'login', 'setup', 'legal'] export default defineNuxtRouteMiddleware(async (to) => { await ensureAuthSessionLoaded() @@ -10,7 +10,7 @@ export default defineNuxtRouteMiddleware(async (to) => { const settings = await ensurePublicSettingsLoaded() if (!settings.value?.require2fa) return - // /account is where enrollment happens; /login and /setup stay reachable regardless. Matched on route name so it survives any i18n strategy. + // /account is where enrollment happens; /login, /setup and the legal pages stay reachable regardless. Matched on route name so it survives any i18n strategy. const routeName = to.name?.toString() ?? '' if (ALLOWED_ROUTE_PREFIXES.some(prefix => routeName.startsWith(prefix))) return diff --git a/apps/app/app/pages/account.vue b/apps/app/app/pages/account.vue index 615f887..4d82f03 100644 --- a/apps/app/app/pages/account.vue +++ b/apps/app/app/pages/account.vue @@ -231,7 +231,7 @@ async function deleteAccount() {