From cc0f44258ab86d24ab9d62b5e9778ecab1d66d3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sat, 18 Apr 2026 00:08:48 +0200 Subject: [PATCH] Add legal pages, ToS acceptance, and alpha banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Journal: Impressum (§5 TMG), Terms of Service, GDPR privacy page - Registration flow: required ToS checkbox, termsAcceptedAt persisted - Alpha banner on Journal (always visible); alpha badge on Planner hero - Footer with legal links + GitHub on both apps - Reduce PII: sendDefaultPii=false, Sentry init only after login (Journal) - Drop Sentry from Planner (no login, no consent possible) - Drop localStorage caching from i18n client detection - Sync SSR i18n resources each request so locale edits HMR without restart Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/journal/app/components/AlphaBanner.tsx | 10 + apps/journal/app/components/Footer.tsx | 33 +++ apps/journal/app/entry.client.tsx | 22 -- apps/journal/app/entry.server.tsx | 1 + apps/journal/app/lib/auth.server.ts | 3 +- apps/journal/app/lib/operator.ts | 20 ++ apps/journal/app/lib/sentry.client.ts | 30 +++ apps/journal/app/root.tsx | 8 +- apps/journal/app/routes.ts | 3 + apps/journal/app/routes/api.auth.register.ts | 8 +- apps/journal/app/routes/auth.login.tsx | 2 +- apps/journal/app/routes/auth.register.tsx | 47 +++- apps/journal/app/routes/legal.imprint.tsx | 94 ++++++++ apps/journal/app/routes/legal.privacy.tsx | 238 +++++++++++++++++++ apps/journal/app/routes/legal.terms.tsx | 187 +++++++++++++++ apps/journal/app/routes/privacy.tsx | 129 +--------- apps/mobile/lib/sentry.ts | 1 + apps/planner/app/entry.client.tsx | 22 -- apps/planner/app/routes/home.tsx | 16 +- openspec/changes/legal-disclaimers/tasks.md | 44 ++-- packages/db/src/schema/journal.ts | 1 + packages/i18n/src/index.ts | 20 +- packages/i18n/src/locales/de.ts | 20 ++ packages/i18n/src/locales/en.ts | 20 ++ 24 files changed, 773 insertions(+), 206 deletions(-) create mode 100644 apps/journal/app/components/AlphaBanner.tsx create mode 100644 apps/journal/app/components/Footer.tsx create mode 100644 apps/journal/app/lib/operator.ts create mode 100644 apps/journal/app/lib/sentry.client.ts create mode 100644 apps/journal/app/routes/legal.imprint.tsx create mode 100644 apps/journal/app/routes/legal.privacy.tsx create mode 100644 apps/journal/app/routes/legal.terms.tsx diff --git a/apps/journal/app/components/AlphaBanner.tsx b/apps/journal/app/components/AlphaBanner.tsx new file mode 100644 index 0000000..16ca223 --- /dev/null +++ b/apps/journal/app/components/AlphaBanner.tsx @@ -0,0 +1,10 @@ +import { useTranslation } from "react-i18next"; + +export function AlphaBanner() { + const { t } = useTranslation("journal"); + return ( +
+

{t("alpha.message")}

+
+ ); +} diff --git a/apps/journal/app/components/Footer.tsx b/apps/journal/app/components/Footer.tsx new file mode 100644 index 0000000..1542c1c --- /dev/null +++ b/apps/journal/app/components/Footer.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from "react-i18next"; + +export function Footer() { + const { t } = useTranslation("journal"); + return ( + + ); +} diff --git a/apps/journal/app/entry.client.tsx b/apps/journal/app/entry.client.tsx index 8e3f019..d5b4d57 100644 --- a/apps/journal/app/entry.client.tsx +++ b/apps/journal/app/entry.client.tsx @@ -1,30 +1,8 @@ -import * as Sentry from "@sentry/react"; -import { useEffect } from "react"; import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; import { HydratedRouter } from "react-router/dom"; -import { useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from "react-router"; import { initI18nClient } from "@trails-cool/i18n"; -const sentryEnvironment = import.meta.env.VITE_SENTRY_ENVIRONMENT ?? - (import.meta.env.PROD ? "production" : "development"); - -Sentry.init({ - dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728", - integrations: [ - Sentry.reactRouterV7BrowserTracingIntegration({ - useEffect, - useLocation, - useNavigationType, - createRoutesFromChildren, - matchRoutes, - }), - ], - environment: sentryEnvironment, - tracesSampleRate: sentryEnvironment === "ci" ? 0 : 1.0, - enabled: import.meta.env.PROD && sentryEnvironment !== "ci", -}); - initI18nClient(); startTransition(() => { diff --git a/apps/journal/app/entry.server.tsx b/apps/journal/app/entry.server.tsx index 0bdfe8f..ff195e8 100644 --- a/apps/journal/app/entry.server.tsx +++ b/apps/journal/app/entry.server.tsx @@ -16,6 +16,7 @@ Sentry.init({ environment: sentryEnvironment, tracesSampleRate: 1.0, enabled: process.env.NODE_ENV === "production" && !process.env.CI, + sendDefaultPii: false, beforeSend(event) { // Drop 404s — they're expected (scanners, typos), not bugs const serialized = event.extra?.__serialized__ as Record | undefined; diff --git a/apps/journal/app/lib/auth.server.ts b/apps/journal/app/lib/auth.server.ts index d44e4cd..204aa64 100644 --- a/apps/journal/app/lib/auth.server.ts +++ b/apps/journal/app/lib/auth.server.ts @@ -76,6 +76,7 @@ export async function finishRegistration( email, username, domain, + termsAcceptedAt: new Date(), }); await db.insert(credentials).values({ @@ -158,7 +159,7 @@ export async function registerWithMagicLink(email: string, username: string): Pr const userId = randomUUID(); const domain = process.env.DOMAIN ?? "localhost"; - await db.insert(users).values({ id: userId, email, username, domain }); + await db.insert(users).values({ id: userId, email, username, domain, termsAcceptedAt: new Date() }); // Create magic token for verification const token = randomBytes(32).toString("base64url"); diff --git a/apps/journal/app/lib/operator.ts b/apps/journal/app/lib/operator.ts new file mode 100644 index 0000000..4ea49af --- /dev/null +++ b/apps/journal/app/lib/operator.ts @@ -0,0 +1,20 @@ +/** + * Operator details for the Impressum (§5 TMG / §18 MStV). + * + * Required by German law. Address must be a reachable physical address. + * Email must be a direct contact (no forms-only policy). + */ +export const operator = { + name: "Ullrich Schäfer", + address: { + street: "Mehringdamm 87", + postalCode: "10965", + city: "Berlin", + country: "Germany", + }, + email: "legal@trails.cool", + // Responsible for content per §18 Abs. 2 MStV + responsiblePerson: "Ullrich Schäfer", +} as const; + +export type Operator = typeof operator; diff --git a/apps/journal/app/lib/sentry.client.ts b/apps/journal/app/lib/sentry.client.ts new file mode 100644 index 0000000..d2e6dac --- /dev/null +++ b/apps/journal/app/lib/sentry.client.ts @@ -0,0 +1,30 @@ +import * as Sentry from "@sentry/react"; +import { useEffect } from "react"; +import { useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from "react-router"; + +let initialized = false; + +export function initSentryClient() { + if (initialized) return; + initialized = true; + + const sentryEnvironment = import.meta.env.VITE_SENTRY_ENVIRONMENT ?? + (import.meta.env.PROD ? "production" : "development"); + + Sentry.init({ + dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728", + integrations: [ + Sentry.reactRouterV7BrowserTracingIntegration({ + useEffect, + useLocation, + useNavigationType, + createRoutesFromChildren, + matchRoutes, + }), + ], + environment: sentryEnvironment, + tracesSampleRate: sentryEnvironment === "ci" ? 0 : 1.0, + enabled: import.meta.env.PROD && sentryEnvironment !== "ci", + sendDefaultPii: false, + }); +} diff --git a/apps/journal/app/root.tsx b/apps/journal/app/root.tsx index be4a5f4..d8967b0 100644 --- a/apps/journal/app/root.tsx +++ b/apps/journal/app/root.tsx @@ -7,6 +7,9 @@ import { useTranslation } from "react-i18next"; import { detectLocale } from "@trails-cool/i18n"; import { getSessionUser } from "~/lib/auth.server"; import { LocaleProvider } from "~/components/LocaleContext"; +import { AlphaBanner } from "~/components/AlphaBanner"; +import { Footer } from "~/components/Footer"; +import { initSentryClient } from "~/lib/sentry.client"; import stylesheet from "@trails-cool/ui/styles.css?url"; export const links: LinksFunction = () => [{ rel: "stylesheet", href: stylesheet }]; @@ -116,7 +119,8 @@ export default function App({ loaderData }: Route.ComponentProps) { const locale = loaderData?.locale ?? "en"; useEffect(() => { if (user) { - Sentry.setUser({ id: user.id, username: user.username }); + initSentryClient(); + Sentry.setUser({ id: user.id }); } else { Sentry.setUser(null); } @@ -124,8 +128,10 @@ export default function App({ loaderData }: Route.ComponentProps) { return ( + +