feat(compliance): P-01 Rechtstextseiten technisch anlegen (0.8.69)
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 32s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 32s

Öffentliche Routen /impressum /datenschutz /nutzungsbedingungen
/medienrichtlinie ohne Auth erreichbar. LegalPage-Komponente mit
deutlichem Platzhalterhinweis und strukturierten Pflichtfeldern je
Rechtstext. Links in LoginPage-Footer und DesktopSidebar-Footer.

KRIT-01 technischer Teil geschlossen. Juristische Inhalte bleiben
offen — Betreiber + Rechtsanwalt erforderlich.

- frontend/src/pages/LegalPage.jsx (neu)
- frontend/src/App.jsx: 4 öffentliche Routen
- frontend/src/pages/LoginPage.jsx: Rechtstext-Links im Footer
- frontend/src/components/DesktopSidebar.jsx: Links im Sidebar-Footer
- tests/dev-smoke-test.spec.js: 5 neue P-01-Tests

version: 0.8.69 (backend + frontend)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-05-10 09:41:45 +02:00
parent d73ed13f87
commit d7ed0c0e9b
7 changed files with 312 additions and 5 deletions

View File

@ -1,6 +1,6 @@
# Shinkan Jinkendo Version Information
APP_VERSION = "0.8.68"
APP_VERSION = "0.8.69"
BUILD_DATE = "2026-05-10"
DB_SCHEMA_VERSION = "20260508049"
@ -29,6 +29,14 @@ MODULE_VERSIONS = {
}
CHANGELOG = [
{
"version": "0.8.69",
"date": "2026-05-10",
"changes": [
"Compliance P-01 (KRIT-01) technischer Teil: Rechtstextseiten /impressum, /datenschutz, /nutzungsbedingungen, /medienrichtlinie als oeffentliche Routen angelegt (Platzhalter, kein Auth erforderlich)",
"Login-Seite und Desktop-Sidebar enthalten Links zu allen vier Rechtstextseiten",
],
},
{
"version": "0.8.68",
"date": "2026-05-10",

View File

@ -37,6 +37,7 @@ import AdminUsersPage from './pages/AdminUsersPage'
import AdminHomeRedirect from './components/AdminHomeRedirect'
import PlatformAdminRoute from './components/PlatformAdminRoute'
import MediaLibraryPage from './pages/MediaLibraryPage'
import LegalPage from './pages/LegalPage'
import ActiveClubSwitcher from './components/ActiveClubSwitcher'
import InactiveMembershipBanner from './components/InactiveMembershipBanner'
import './app.css'
@ -171,6 +172,12 @@ function AppRoutes() {
}
/>
{/* P-01: Öffentliche Rechtstextseiten — kein Auth erforderlich */}
<Route path="/impressum" element={<LegalPage type="impressum" />} />
<Route path="/datenschutz" element={<LegalPage type="datenschutz" />} />
<Route path="/nutzungsbedingungen" element={<LegalPage type="nutzungsbedingungen" />} />
<Route path="/medienrichtlinie" element={<LegalPage type="medienrichtlinie" />} />
<Route element={<ProtectedLayout />}>
<Route index element={<Dashboard />} />
<Route path="profile" element={<Navigate to="/settings" replace />} />

View File

@ -1,4 +1,4 @@
import { NavLink, useLocation } from 'react-router-dom'
import { NavLink, Link, useLocation } from 'react-router-dom'
import { LogOut } from 'lucide-react'
import { getMainNavItems } from '../config/appNav'
import { useOrgInbox } from '../context/OrgInboxContext'
@ -92,6 +92,20 @@ export default function DesktopSidebar({
</button>
</div>
</div>
<div
style={{
padding: '0.5rem 1rem 0.75rem',
display: 'flex',
gap: '0.6rem',
flexWrap: 'wrap',
fontSize: '0.7rem',
}}
>
<Link to="/impressum" style={{ color: 'var(--text3)' }}>Impressum</Link>
<Link to="/datenschutz" style={{ color: 'var(--text3)' }}>Datenschutz</Link>
<Link to="/nutzungsbedingungen" style={{ color: 'var(--text3)' }}>Nutzungsbedingungen</Link>
<Link to="/medienrichtlinie" style={{ color: 'var(--text3)' }}>Medienrichtlinie</Link>
</div>
</aside>
)
}

View File

@ -0,0 +1,212 @@
import { Link } from 'react-router-dom'
const PAGES = {
impressum: {
title: 'Impressum',
sections: [
{
heading: 'Betreiber / Verantwortlicher',
placeholder: '[Name und Rechtsform des Betreibers — vom Betreiber einzutragen]',
},
{
heading: 'Anschrift',
placeholder: '[Straße, Hausnummer, PLZ, Ort — vom Betreiber einzutragen]',
},
{
heading: 'Vertretungsberechtigte Person',
placeholder: '[Name der vertretungsberechtigten Person — vom Betreiber einzutragen]',
},
{
heading: 'Kontakt',
placeholder: '[E-Mail-Adresse, ggf. Telefonnummer — vom Betreiber einzutragen]',
},
{
heading: 'Registerangaben (falls relevant)',
placeholder: '[Vereinsregister, Handelsregister o. ä. — vom Rechtsanwalt prüfen lassen]',
},
],
},
datenschutz: {
title: 'Datenschutzerklärung',
sections: [
{
heading: 'Verantwortlicher',
placeholder: '[Name, Anschrift und Kontakt des Verantwortlichen — vom Betreiber einzutragen]',
},
{
heading: 'Zwecke der Verarbeitung',
placeholder: '[Welche Daten werden zu welchem Zweck verarbeitet? — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Rechtsgrundlagen',
placeholder: '[Art. 6 DSGVO: Einwilligung, Vertrag, berechtigtes Interesse — vom Rechtsanwalt zu bestimmen]',
},
{
heading: 'Empfänger und Dienstleister',
placeholder: '[SMTP-Anbieter, Hosting, ggf. weitere — vom Rechtsanwalt und Betreiber zu listen]',
},
{
heading: 'Speicherdauern',
placeholder: '[Wie lange werden welche Daten gespeichert? — vom Rechtsanwalt zu bestimmen]',
},
{
heading: 'Betroffenenrechte',
placeholder: '[Auskunft, Berichtigung, Löschung, Widerspruch, Datenübertragbarkeit — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Browser-Speicher (localStorage, sessionStorage)',
placeholder: '[Technisch notwendige Speicherung des Auth-Tokens und Sitzungsdaten. Nach TDDDG §25 ggf. ohne Einwilligung zulässig — vom Rechtsanwalt zu prüfen]',
},
{
heading: 'Kontakt für Datenschutzanfragen',
placeholder: '[E-Mail oder Kontaktweg für Datenschutzanfragen — vom Betreiber einzutragen]',
},
{
heading: 'Zuständige Aufsichtsbehörde',
placeholder: '[Zuständige Datenschutzbehörde je nach Bundesland — vom Rechtsanwalt zu bestimmen]',
},
],
},
nutzungsbedingungen: {
title: 'Nutzungsbedingungen',
sections: [
{
heading: 'Nutzungsumfang',
placeholder: '[Für welche Nutzergruppen und Zwecke darf die Plattform genutzt werden? — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Registrierung',
placeholder: '[Voraussetzungen für die Registrierung, Wahrheitspflicht der Angaben — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Zulässige und unzulässige Inhalte',
placeholder: '[Was darf hochgeladen und veröffentlicht werden? Was ist verboten? — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Verantwortlichkeit der Nutzer',
placeholder: '[Nutzer sind für ihre hochgeladenen Inhalte selbst verantwortlich — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Sperrung und Löschung',
placeholder: '[Unter welchen Bedingungen können Konten oder Inhalte gesperrt oder gelöscht werden? — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Haftungshinweise',
placeholder: '[Haftungsausschluss für Nutzerinhalte, externe Links, Systemausfälle — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Geltungsbereich und anwendbares Recht',
placeholder: '[Welches Recht ist anwendbar? Welcher Gerichtsstand gilt? — vom Rechtsanwalt zu bestimmen]',
},
],
},
medienrichtlinie: {
title: 'Medienrichtlinie',
sections: [
{
heading: 'Urheberrechte',
placeholder: '[Nur eigene oder ausdrücklich lizenzierte Inhalte hochladen — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Rechte am eigenen Bild (§ 22 KUG)',
placeholder: '[Erkennbare Personen müssen eingewilligt haben; besondere Regeln für Minderjährige — juristisch zu prüfen und zu formulieren]',
},
{
heading: 'Minderjährige',
placeholder: '[Besondere Schutzpflichten bei Aufnahmen von Minderjährigen — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Musik und sonstige Fremdinhalte',
placeholder: '[Keine Hintergrundmusik oder andere Fremdinhalte ohne gültige Lizenz — vom Rechtsanwalt zu formulieren]',
},
{
heading: 'Sichtbarkeitsstufen',
placeholder: '[Erläuterung der Stufen: privat, vereinsintern, öffentlich — vom Betreiber zu beschreiben]',
},
{
heading: 'Meldewege für rechtsverletzende Inhalte',
placeholder: '[Wie können Inhalte gemeldet werden? — wird nach Umsetzung von P-13 (Content-Melde-Backend) ergänzt]',
},
{
heading: 'Lösch- und Sperrlogik',
placeholder: '[Wann und wie werden gemeldete Inhalte entfernt oder gesperrt? — wird nach Umsetzung von P-11 und P-13 ergänzt]',
},
],
},
}
const LEGAL_LINKS = [
{ to: '/impressum', label: 'Impressum' },
{ to: '/datenschutz', label: 'Datenschutz' },
{ to: '/nutzungsbedingungen', label: 'Nutzungsbedingungen' },
{ to: '/medienrichtlinie', label: 'Medienrichtlinie' },
]
function LegalPage({ type }) {
const page = PAGES[type]
if (!page) return null
return (
<div style={{ minHeight: '100vh', background: 'var(--bg)', padding: '2rem 1rem' }}>
<div style={{ maxWidth: '720px', margin: '0 auto' }}>
<div style={{ marginBottom: '1.5rem' }}>
<Link to="/login" style={{ color: 'var(--accent)', textDecoration: 'none', fontSize: '0.9rem' }}>
Zurück zur Anmeldung
</Link>
</div>
<div
className="card"
style={{
marginBottom: '1.5rem',
borderLeft: '4px solid var(--danger)',
background: 'var(--surface)',
}}
>
<strong style={{ color: 'var(--danger)' }}> MUSTER / PLATZHALTER</strong>
<p style={{ margin: '0.5rem 0 0', color: 'var(--text2)', fontSize: '0.9rem' }}>
Inhalt wird vor Produktivbetrieb juristisch geprüft und durch den Betreiber ergänzt.
Diese Seite hat keinen rechtlich verbindlichen Charakter.
</p>
</div>
<h1 style={{ marginBottom: '2rem', color: 'var(--text1)' }}>{page.title}</h1>
{page.sections.map((section) => (
<div key={section.heading} style={{ marginBottom: '1.75rem' }}>
<h2 style={{ fontSize: '1.05rem', marginBottom: '0.4rem', color: 'var(--text1)' }}>
{section.heading}
</h2>
<p style={{ color: 'var(--text3)', fontStyle: 'italic', margin: 0 }}>
{section.placeholder}
</p>
</div>
))}
<div
style={{
marginTop: '3rem',
paddingTop: '1rem',
borderTop: '1px solid var(--border)',
fontSize: '0.82rem',
color: 'var(--text3)',
textAlign: 'center',
display: 'flex',
gap: '1.25rem',
justifyContent: 'center',
flexWrap: 'wrap',
}}
>
{LEGAL_LINKS.map((l) => (
<Link key={l.to} to={l.to} style={{ color: 'var(--text3)' }}>
{l.label}
</Link>
))}
</div>
</div>
</div>
)
}
export default LegalPage

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNavigate, Link } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import api from '../utils/api'
@ -238,6 +238,25 @@ function LoginPage() {
</p>
</div>
)}
<div
style={{
marginTop: '1.5rem',
paddingTop: '1rem',
borderTop: '1px solid var(--border)',
fontSize: '0.78rem',
color: 'var(--text3)',
textAlign: 'center',
display: 'flex',
gap: '1rem',
justifyContent: 'center',
flexWrap: 'wrap',
}}
>
<Link to="/impressum" style={{ color: 'var(--text3)' }}>Impressum</Link>
<Link to="/datenschutz" style={{ color: 'var(--text3)' }}>Datenschutz</Link>
<Link to="/nutzungsbedingungen" style={{ color: 'var(--text3)' }}>Nutzungsbedingungen</Link>
<Link to="/medienrichtlinie" style={{ color: 'var(--text3)' }}>Medienrichtlinie</Link>
</div>
</div>
</div>
)

View File

@ -1,10 +1,11 @@
// Shinkan Jinkendo Frontend Version
export const APP_VERSION = "0.8.68"
export const APP_VERSION = "0.8.69"
export const BUILD_DATE = "2026-05-10"
export const PAGE_VERSIONS = {
LoginPage: "1.0.1",
LoginPage: "1.0.2",
LegalPage: "1.0.0",
Dashboard: "1.0.0",
AccountSettingsPage: "1.0.1",
ExercisesPage: "1.5.0", // Fokus +/- Regeln, nur ohne Fokusbereich; Filterprefs

View File

@ -184,6 +184,52 @@ test('P-12: sessionStorage wird bei Logout bereinigt (sj_coach_* Schlüssel)', a
console.log('✓ P-12: sj_coach_* entfernt, Fremd-Key erhalten, authToken entfernt');
});
// P-01: Rechtstextseiten öffentliche Routen ohne Auth
const LEGAL_ROUTES = [
{ path: '/impressum', label: 'Impressum' },
{ path: '/datenschutz', label: 'Datenschutz' },
{ path: '/nutzungsbedingungen', label: 'Nutzungsbedingungen' },
{ path: '/medienrichtlinie', label: 'Medienrichtlinie' },
];
for (const route of LEGAL_ROUTES) {
test(`P-01: ${route.label} ohne Auth erreichbar und enthält Platzhalterhinweis`, async ({ page }) => {
// Direkt aufrufen ohne Login
await page.goto(route.path);
await page.waitForLoadState('networkidle');
// Seite ist erreichbar (kein Redirect zur Login-Seite)
expect(page.url()).toContain(route.path);
// Platzhalterhinweis sichtbar
const hinweis = await page.getByText('MUSTER / PLATZHALTER').first();
await expect(hinweis).toBeVisible();
// Seitentitel korrekt
await expect(page.getByRole('heading', { level: 1 })).toContainText(route.label);
// Reload funktioniert
await page.reload();
await page.waitForLoadState('networkidle');
expect(page.url()).toContain(route.path);
console.log(`✓ P-01: ${route.label} ohne Auth erreichbar, Platzhalter sichtbar, Reload OK`);
});
}
test('P-01: Login-Seite enthält Links zu allen vier Rechtstextseiten', async ({ page }) => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
for (const route of LEGAL_ROUTES) {
const link = page.locator(`a[href="${route.path}"]`);
await expect(link).toBeVisible();
}
console.log('✓ P-01: Login-Seite alle vier Rechtstext-Links vorhanden');
});
test('8. Keine kritischen Console-Fehler', async ({ page }) => {
const errors = [];
page.on('console', msg => {