feat(web): add legal pages (ToS, Privacy Policy, Impressum) with DSGVO compliance#333
Conversation
Adds public /terms and /privacy routes with full legal content pages matching the app's dark theme. Links appear in the login card footer and in the sidebar footer (when expanded). Adds i18n keys for EN/DE. Co-Authored-By: Claude <noreply@anthropic.com>
…move sidebar links - Extract duplicated CSS/template from TermsView and PrivacyView into LegalPageLayout.vue component; each view now ~30 lines of content only - Replace inline SVG back-arrows with <Icon icon="mdi:arrow-left" /> consistent with the rest of the codebase - Remove legal links from sidebar footer (poor fit); login page only Co-Authored-By: Claude <noreply@anthropic.com>
- Add DE content to TermsView and PrivacyView via v-if/v-else on locale - Add LanguageSwitcher to LegalPageLayout (top-right, same as login page) - Move hardcoded "Last updated" date into i18n (legal.last_updated, EN/DE) - Add position: relative to .legal-card to anchor the switcher Co-Authored-By: Claude <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughAdds a LegalPageLayout component, three legal views, i18n translations for a new Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Browser as Browser
participant View as Legal View
participant Composable as useLegalContact
participant API as API /api/legal/contact
participant Config as WebConfig
rect rgba(200,200,255,0.5)
Browser->>View: Navigate to /privacy (or /terms, /impressum)
end
rect rgba(200,255,200,0.5)
View->>Composable: call useLegalContact()
Composable->>API: GET /api/legal/contact
end
rect rgba(255,200,200,0.5)
API->>Config: read WebConfig (dependency)
Config-->>API: return legal_* values
API-->>Composable: 200 {enabled,name,street,zip_city,country_*,email}
Composable-->>View: provide reactive contact data
View-->>Browser: render legal page with contact info and translations
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/frontend/src/views/PrivacyView.vue`:
- Around line 14-166: The template duplicates the entire policy for German and
default locales (see the two <template v-if="locale.current === 'de'"> and
<template v-else> blocks in PrivacyView.vue), which risks drift; refactor by
extracting each static section and text into i18n keys (or a localized content
object) and render a single template that iterates/outputs those keys (keep
RouterLink usage intact), replacing hardcoded German/English strings with
$t('privacy.sectionX.title') / $t('privacy.sectionX.body') or a localized
sections array so both locales share the same structure and updates stay
synchronized.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: a6515c68-e8dc-4cdb-842c-baa7514a0d91
📒 Files selected for processing (9)
web/frontend/src/components/LegalPageLayout.vueweb/frontend/src/i18n/locales/de.tsweb/frontend/src/i18n/locales/en.tsweb/frontend/src/router/index.tsweb/frontend/src/views/ImpressumView.vueweb/frontend/src/views/LoginView.vueweb/frontend/src/views/PrivacyView.vueweb/frontend/src/views/TermsView.vueweb/frontend/src/views/guild/GuildDetailView.vue
Add /impressum route with §5 DDG and §18 MStV required fields. Update Privacy Policy with DSGVO Art. 13 mandatory disclosures (data controller identity, legal basis, full rights catalogue, supervisory authority, third- country transfers). Update Terms of Service with advance-notice clause for changes, governing law/jurisdiction, and ODR platform reference. Add Impressum link to login page and all legal page footers. Upgrade LegalPageLayout to support multiple footer links and optional meta line. Co-Authored-By: Claude <noreply@anthropic.com> refactor(web): extract legal page content into i18n keys + runtime contact API - Add legal contact fields to WebConfig (legal_name, legal_street, legal_zip_city, legal_country_en/de, legal_email) with NERPYBOT_WEB_LEGAL_* env var mappings - Add LegalContactResponse schema and public GET /api/legal/contact endpoint (no auth required) - Add useLegalContact composable — fetches contact data once, caches at module scope, leaves empty strings on failure - Extract all page text into i18n keys under legal.impressum_page, legal.terms_page, legal.privacy_page namespaces (EN + DE) - Refactor ImpressumView, TermsView, PrivacyView to single templates using t() + useLegalContact(); RouterLink-split keys for inline link paragraphs; v-html for bold-formatted list items Co-Authored-By: Claude <noreply@anthropic.com>
9d5fed7 to
ee281a4
Compare
…se.yml Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/config.py`:
- Around line 98-103: The legal contact fields (legal_name, legal_street,
legal_zip_city, legal_country_en, legal_country_de, legal_email) are currently
loaded as optional and can be empty; update the config loading in web/config.py
to validate these values after _get is called and before returning/exposing the
config: check each of the listed variables for a non-empty string and, if any
are missing/empty, raise a clear configuration error (or log and abort startup)
so the application fails fast instead of serving blank /api/legal/contact
content. Ensure the validation references the exact variable names above so
maintainers can locate the checks easily.
In `@web/frontend/src/composables/useLegalContact.ts`:
- Around line 29-38: The bug sets the module-level flag fetched before the
network call completes, causing transient failures to be cached; change the
logic in useLegalContact (the fetched flag and the fetch("/api/legal/contact")
promise chain that assigns to contact) so that fetched is only set after a
successful response/JSON parse and Object.assign(contact, data) completes (or
set fetched in the success handler), and ensure the catch handler does not flip
fetched to true; this will let subsequent calls retry on earlier failures while
preserving the successful-case behavior.
In `@web/frontend/src/i18n/locales/de.ts`:
- Line 128: The long German legal string value ("Wir behalten uns das Recht vor,
NerpyBot ... Wir garantieren keine kontinuierliche, ununterbrochene
Verfügbarkeit des Dienstes.") exceeds the 120-character limit; split this string
into multiple shorter string literals concatenated (or use a template literal
without inserting an actual newline) so no source line exceeds 120 chars,
preserving spaces and punctuation and keeping the same key in the de.ts locale
object; apply the same wrapping approach to the other long entries mentioned
(the strings at lines 183 and 205) to comply with the project's line-length
rule.
In `@web/frontend/src/i18n/locales/en.ts`:
- Line 126: The long legal sentence in web/frontend/src/i18n/locales/en.ts ("We
reserve the right to remove NerpyBot from any server or revoke dashboard access,
particularly in cases of abuse or violation of these terms. Except in cases of
serious violations or abuse, we will provide reasonable advance notice. We do
not guarantee continuous, uninterrupted availability of the service.") exceeds
the 120-character line length rule; edit that string so no source line is longer
than 120 characters by breaking it into multiple shorter string literals
concatenated (or into a template literal kept within 120 chars per source line),
ensuring you update the exact string entry in en.ts and keep the original text
unchanged when concatenated.
In `@web/frontend/src/views/ImpressumView.vue`:
- Around line 20-23: The ImpressumView.vue template renders legal contact fields
unconditionally (using the contact object fields like contact.name,
contact.street, contact.zip_city, contact.country_de/country_en), which can
produce blank/missing mandatory details if /api/legal/contact is empty; update
the template to guard rendering with a top-level condition (e.g., check contact
is defined and has required properties) or render a clear fallback/placeholder
so the compliance block never shows empty lines, and apply the same guard to the
other similar block around the 29-37 region.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 824b6c60-1157-4831-9a5a-6b312066199d
📒 Files selected for processing (13)
web/app.pyweb/config.pyweb/frontend/src/components/LegalPageLayout.vueweb/frontend/src/composables/useLegalContact.tsweb/frontend/src/i18n/locales/de.tsweb/frontend/src/i18n/locales/en.tsweb/frontend/src/router/index.tsweb/frontend/src/views/ImpressumView.vueweb/frontend/src/views/LoginView.vueweb/frontend/src/views/PrivacyView.vueweb/frontend/src/views/TermsView.vueweb/routes/legal.pyweb/schemas.py
- Add Biome 2.4.7 as frontend linter with recommended rules at repo root (biome.json); disable noUnusedVariables/noUnusedImports for Vue SFCs (Biome can't see template usage); disable CSS linter (Tailwind uses non-standard syntax) - Apply Biome safe fixes across all frontend source files (import ordering, quote style, semicolons, line width) - Add biome-check pre-commit hook alongside existing ruff and prettier - Add `enabled` flag to LegalContactResponse schema and compute it from all 6 legal fields being non-empty; self-hosted deployments without legal env vars get enabled=false - Add `enabled: boolean` to useLegalContact composable with false default; fix fetched-race by moving `fetched = true` into the .then() success handler so failed fetches can retry - Wrap login page legal links in v-if="contact.enabled" so they only appear when legal contact is configured - Guard contact-data sections in ImpressumView and controller block in PrivacyView with v-if="contact.name" to avoid blank sections Co-Authored-By: Claude <noreply@anthropic.com>
- Replace boolean `fetched` flag with Promise-based deduplication in useLegalContact: concurrent callers share one in-flight Promise, failures clear the reference so the next caller can retry cleanly - Use contact.enabled (all 6 fields present) instead of contact.name as the visibility guard in ImpressumView and PrivacyView, consistent with LoginView — partial configs (name set, email missing) no longer render incomplete contact blocks Co-Authored-By: Claude <noreply@anthropic.com>
Check res.ok before parsing JSON so non-2xx responses don't silently produce garbage state. Map each field explicitly with String()/Boolean() coercion instead of blindly spreading the parsed payload. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
web/frontend/src/views/ImpressumView.vue (1)
17-41:⚠️ Potential issue | 🟠 MajorAvoid silently serving an incomplete Impressum when contact data is unavailable.
At Line 17,
v-if="contact.enabled"hides all mandatory legal identity/contact sections. If config/fetch fails, the page still renders with only disclaimer text, which is a compliance risk.Proposed hardening
- <template v-if="contact.enabled"> + <template v-if="contact.enabled"> <section> <h2>{{ t('legal.impressum_page.legal_info_title') }}</h2> <p> {{ contact.name }}<br> {{ contact.street }}<br> {{ contact.zip_city }}<br> {{ locale.current === 'de' ? contact.country_de : contact.country_en }} </p> </section> @@ <section> <h2>{{ t('legal.impressum_page.responsible_title') }}</h2> <p> {{ contact.name }}<br> {{ contact.street }}<br> {{ contact.zip_city }} </p> </section> </template> + <section v-else> + <h2>{{ t('legal.impressum_page.legal_info_title') }}</h2> + <p>{{ t('common.load_failed') }}</p> + </section>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/frontend/src/views/ImpressumView.vue` around lines 17 - 41, The template currently only checks contact.enabled which lets the page render partial/disclaimer content if contact fetch fails; add a computed/property (e.g., isContactAvailable) in ImpressumView.vue that validates contact exists, contact.enabled is true and required fields (name, street, zip_city, email) are non-empty, then replace v-if="contact.enabled" with v-if="isContactAvailable"; if isContactAvailable is false, render a clear fallback/warning instead of the partial Impressum (and guard uses of locale.current when selecting country text) so incomplete contact data is never silently served.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@web/frontend/src/composables/useLegalContact.ts`:
- Around line 2-4: The header comment is incorrect about when the fetch runs;
update the comment for useLegalContact to state that the fetch is triggered on
the first invocation of useLegalContact() (lazy on-demand), not at module scope,
and clarify that it caches the result across subsequent calls and leaves empty
strings on fetch failure; reference the useLegalContact function and any
module-level cache variables to ensure the wording matches the actual
implementation.
In `@web/frontend/src/views/LoginView.vue`:
- Around line 6-7: The login view currently hides legal navigation when the
composable useLegalContact indicates contact.enabled is false; instead always
render/show legal links regardless of contact.enabled to avoid hiding legal
pages on transient API failure — update the logic in LoginView.vue (where
useLegalContact is used and where conditional rendering appears around
contact.enabled, including the other occurrences noted at the second occurrence
and lines ~139-146) to separate the legal navigation UI from contact.enabled
(use contact.enabled only for contact-specific controls, not for showing the
legal links), ensuring legal links are always visible to unauthenticated users.
In `@web/routes/legal.py`:
- Around line 16-34: The enabled calculation currently treats whitespace-only
config values as truthy; before computing enabled in the function that returns
LegalContactResponse, normalize each config field (config.legal_name,
config.legal_street, config.legal_zip_city, config.legal_country_en,
config.legal_country_de, config.legal_email) by trimming whitespace (e.g.,
.strip() or equivalent) and use the trimmed values to compute enabled and to
populate the LegalContactResponse fields so that fields containing only
whitespace are considered empty/false.
---
Duplicate comments:
In `@web/frontend/src/views/ImpressumView.vue`:
- Around line 17-41: The template currently only checks contact.enabled which
lets the page render partial/disclaimer content if contact fetch fails; add a
computed/property (e.g., isContactAvailable) in ImpressumView.vue that validates
contact exists, contact.enabled is true and required fields (name, street,
zip_city, email) are non-empty, then replace v-if="contact.enabled" with
v-if="isContactAvailable"; if isContactAvailable is false, render a clear
fallback/warning instead of the partial Impressum (and guard uses of
locale.current when selecting country text) so incomplete contact data is never
silently served.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9d7dab47-0a8e-4043-a8db-e1fa7567e1a8
⛔ Files ignored due to path filters (1)
web/frontend/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (50)
.pre-commit-config.yamlbiome.jsonweb/frontend/package.jsonweb/frontend/src/api/client.tsweb/frontend/src/components/DiscordPicker.vueweb/frontend/src/components/HelloWorld.vueweb/frontend/src/components/InfoTooltip.vueweb/frontend/src/components/LanguageSwitcher.vueweb/frontend/src/components/LegalPageLayout.vueweb/frontend/src/components/MockupToolbar.vueweb/frontend/src/components/RealmPicker.vueweb/frontend/src/composables/useAutoSave.tsweb/frontend/src/composables/useLegalContact.tsweb/frontend/src/composables/useManualSave.tsweb/frontend/src/dev/fixtures.tsweb/frontend/src/dev/resolver.tsweb/frontend/src/i18n/index.tsweb/frontend/src/i18n/locales/de.tsweb/frontend/src/i18n/locales/en.tsweb/frontend/src/main.tsweb/frontend/src/router/index.tsweb/frontend/src/stores/auth.tsweb/frontend/src/utils/route.tsweb/frontend/src/views/ImpressumView.vueweb/frontend/src/views/LoginView.vueweb/frontend/src/views/PrivacyView.vueweb/frontend/src/views/TermsView.vueweb/frontend/src/views/guild/FormSubmissionsView.vueweb/frontend/src/views/guild/GuildDetailView.vueweb/frontend/src/views/guild/tabs/ApplicationFormsTab.vueweb/frontend/src/views/guild/tabs/ApplicationSubmissionsTab.vueweb/frontend/src/views/guild/tabs/ApplicationTemplatesTab.vueweb/frontend/src/views/guild/tabs/AutoDeleteTab.vueweb/frontend/src/views/guild/tabs/AutoKickerTab.vueweb/frontend/src/views/guild/tabs/LanguageTab.vueweb/frontend/src/views/guild/tabs/LeaveMessagesTab.vueweb/frontend/src/views/guild/tabs/ModeratorRolesTab.vueweb/frontend/src/views/guild/tabs/OperatorDashboardTab.vueweb/frontend/src/views/guild/tabs/OperatorGuildsTab.vueweb/frontend/src/views/guild/tabs/OperatorModulesTab.vueweb/frontend/src/views/guild/tabs/OperatorUserManagementTab.vueweb/frontend/src/views/guild/tabs/ReactionRolesTab.vueweb/frontend/src/views/guild/tabs/RemindersTab.vueweb/frontend/src/views/guild/tabs/RoleMappingsTab.vueweb/frontend/src/views/guild/tabs/ServerOverviewTab.vueweb/frontend/src/views/guild/tabs/SupportTab.vueweb/frontend/src/views/guild/tabs/WowCraftingTab.vueweb/frontend/src/views/guild/tabs/WowGuildNewsTab.vueweb/routes/legal.pyweb/schemas.py
- useLegalContact: fix comment — fetch is lazy (first call), not module-scope; clarify fetchPromise/contact caching and retry-on-failure behaviour - LoginView: always render legal navigation links regardless of contact.enabled to avoid hiding them on transient API failures; remove now-unused useLegalContact - legal.py: strip whitespace from all config fields before computing enabled and before populating the response, so whitespace-only values are treated as empty - ImpressumView: add isContactAvailable computed that validates contact.enabled and required fields (name, street, zip_city, email) are non-empty; replace v-if="contact.enabled" with v-if="isContactAvailable" Co-Authored-By: Claude <noreply@anthropic.com>
Replace the implicit enabled-from-all-fields heuristic with an explicit NERPYBOT_WEB_LEGAL_ENABLED=true flag. When set, startup validates all six contact fields via _require() and raises ValueError on the first missing one. When unset (default), contact fields are ignored entirely. This removes the ambiguity of "are legal pages on or off?" — the operator must explicitly opt in. The per-request enabled computation in legal.py is replaced by a direct config.legal_enabled read. LoginView restores v-if="contact.enabled" since the flag is now unambiguous. docker-compose files updated with the new var. Co-Authored-By: Claude <noreply@anthropic.com>
Both ImpressumView and PrivacyView need the same contact availability check (enabled + required fields non-empty). Moving it into the composable and returning it alongside contact ensures one definition is shared, so the two views can't drift out of sync. Co-Authored-By: Claude <noreply@anthropic.com>
Summary
/terms,/privacy, and/impressumroutes required for Discord bot verification and German law complianceLegalPageLayoutcomponent withfooterLinksarray prop and optionalshowMetaTest plan
/terms,/privacy,/impressumwithout being logged in — all render correctly/impressumhas no "Last updated" meta linenpm run build --prefix web/frontend🤖 Generated with Claude Code
Summary by CodeRabbit