All checks were successful
Container-Image bauen und veröffentlichen / build-and-push (push) Successful in 1m33s
Der Player wird als iframe eingebunden, aber nicht als rohes HTML aus dem Textfeld: Die Anwendung setzt die Attribute, im Admin steht nur die Adresse. Damit laesst sich ueber das Feld nichts anderes einschleusen. Freigegeben ist alangu.de samt Unterdomains, sonst nichts. Die Pruefung haengt am Hostnamen, nicht an einem Textvergleich - getestet mit boese-alangu.de.angreifer.example und evil.example/?a=alangu.de, beide werden abgewiesen, ebenso http statt https. Vom uebergebenen Einbettungscode weggelassen: accelerometer und gyroscope. Ein Videoplayer braucht keine Bewegungssensoren. Ausserdem: Die Gebaerdensprach-Seite erscheint jetzt auch ohne Text, wenn eine Videoadresse hinterlegt ist - dort ist das Video der Inhalt. Fusszeile und Seite entscheiden das ueber dieselbe Funktion, sonst gaebe es eine erreichbare Seite ohne Link. Geprueft am echten Player: laedt mit HTTP 200 samt CSS, JS und hls.js, axe-core meldet in hell und dunkel keine Verstoesse, der Rahmen ist per Tastatur erreichbar, und die Fusszeile verlinkt die Seite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
344 lines
12 KiB
TypeScript
344 lines
12 KiB
TypeScript
"use client"
|
||
|
||
import { useEffect, useState } from "react"
|
||
import { AlertCircle, ExternalLink } from "lucide-react"
|
||
import Link from "next/link"
|
||
import { toast } from "sonner"
|
||
|
||
import { Button } from "@/components/ui/button"
|
||
import {
|
||
Dialog,
|
||
DialogBody,
|
||
DialogContent,
|
||
DialogDescription,
|
||
DialogFooter,
|
||
DialogHeader,
|
||
DialogTitle,
|
||
} from "@/components/ui/dialog"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
||
import { MarkdownInput } from "@/components/markdown-input"
|
||
import { updateSitePages, useSitePages } from "@/hooks/use-site-pages"
|
||
import { ApiError } from "@/lib/api"
|
||
import {
|
||
EMPTY_SITE_PAGES,
|
||
SITE_PAGES,
|
||
SITE_PAGE_META,
|
||
SitePageKey,
|
||
SitePagesContent,
|
||
} from "@/lib/site-pages"
|
||
|
||
/**
|
||
* Vorlagen als Platzhalter. Sie werden nicht gespeichert, sondern zeigen nur,
|
||
* welche Angaben die jeweilige Seite enthalten muss.
|
||
*/
|
||
const placeholders: Record<SitePageKey, string> = {
|
||
imprint: `## Angaben gemäß § 5 DDG
|
||
|
||
**Amt Musterstadt**
|
||
Musterstraße 1
|
||
12345 Musterstadt
|
||
|
||
**Vertreten durch:** Amtsvorsteher/in Vorname Nachname
|
||
|
||
**Kontakt**
|
||
Telefon: 01234 5678-0
|
||
E-Mail: info@amt-musterstadt.de
|
||
|
||
**Zuständige Aufsichtsbehörde**
|
||
Name und Anschrift der Rechtsaufsicht
|
||
|
||
**Verantwortlich für den Inhalt:** Vorname Nachname, Anschrift`,
|
||
|
||
privacy: `## Datenschutzerklärung für dieses Portal
|
||
|
||
Diese Erklärung betrifft die Verarbeitung Ihrer Daten beim **Besuch dieses
|
||
Portals**. Die Merkblätter zu den einzelnen Verwaltungsverfahren finden Sie in
|
||
der Übersicht.
|
||
|
||
### Verantwortlicher
|
||
Name, Anschrift, Kontakt
|
||
|
||
### Datenschutzbeauftragte/r
|
||
Name, Anschrift, E-Mail
|
||
|
||
### Server-Protokolldaten
|
||
Beim Aufruf werden IP-Adresse, Zeitpunkt, aufgerufene Seite, übertragene
|
||
Datenmenge und Browserkennung protokolliert.
|
||
*Zweck:* technischer Betrieb und Abwehr von Angriffen
|
||
*Rechtsgrundlage:* Art. 6 Abs. 1 lit. e DSGVO i. V. m. Landesrecht
|
||
*Speicherdauer:* z. B. 7 Tage
|
||
|
||
### Cookies
|
||
Ohne Anmeldung setzt das Portal keine Cookies. Im Admin-Bereich wird ein
|
||
technisch notwendiges Sitzungs-Cookie verwendet.
|
||
|
||
### Ihre Rechte
|
||
Auskunft (Art. 15), Berichtigung (Art. 16), Löschung (Art. 17), Einschränkung
|
||
(Art. 18), Widerspruch (Art. 21) sowie Beschwerde bei der Aufsichtsbehörde
|
||
(Art. 77 DSGVO).`,
|
||
|
||
accessibility: `## Erklärung zur Barrierefreiheit
|
||
|
||
Diese Erklärung gilt für die Website **datenschutz.beispiel.de**.
|
||
|
||
### Stand der Vereinbarkeit mit den Anforderungen
|
||
Diese Website ist mit der BITV 2.0 *[vollständig / teilweise / nicht]*
|
||
vereinbar.
|
||
|
||
### Nicht barrierefreie Inhalte
|
||
Hier die bekannten Einschränkungen und deren Begründung auflisten.
|
||
|
||
### Erstellung dieser Erklärung
|
||
Diese Erklärung wurde am TT.MM.JJJJ erstellt.
|
||
Grundlage war eine *[Selbstbewertung / Prüfung durch Dritte]*.
|
||
Letzte Überprüfung: TT.MM.JJJJ
|
||
|
||
### Barrieren melden – Kontakt
|
||
Sie können uns Mängel bei der Barrierefreiheit mitteilen und Informationen in
|
||
barrierefreier Form anfordern:
|
||
Name, E-Mail, Telefon
|
||
|
||
### Schlichtungsverfahren
|
||
Wenn wir auf Ihre Meldung nicht zufriedenstellend reagieren, können Sie die
|
||
Schlichtungsstelle nach § 16 BGG anrufen:
|
||
Anschrift und Kontakt der zuständigen Schlichtungsstelle`,
|
||
|
||
easyLanguage: `## Willkommen
|
||
|
||
Das hier ist die Internet-Seite vom Amt Musterstadt.
|
||
Hier steht: Was macht das Amt mit Ihren Daten?
|
||
|
||
## Was finden Sie auf dieser Seite?
|
||
|
||
Sie finden hier eine Liste.
|
||
In der Liste stehen viele Themen.
|
||
Zum Beispiel: Bau-Antrag. Oder: Wohn-Geld.
|
||
Zu jedem Thema gibt es einen Text.
|
||
In dem Text steht: Welche Daten braucht das Amt?
|
||
Und: Wie lange speichert das Amt die Daten?
|
||
|
||
## Wie finden Sie ein Thema?
|
||
|
||
Oben auf der Seite ist ein Such-Feld.
|
||
Dort können Sie ein Wort eintippen.
|
||
Dann zeigt die Seite nur noch passende Themen.
|
||
Sie können auch auf ein Thema in der Liste klicken.
|
||
|
||
## Barriere-Freiheit
|
||
|
||
Barriere-frei heißt: Alle Menschen können die Seite benutzen.
|
||
Wir arbeiten daran, dass die Seite barriere-frei wird.
|
||
Manche Sachen sind noch nicht barriere-frei.
|
||
Sie können uns das melden.
|
||
Schreiben Sie an: *[E-Mail-Adresse eintragen]*
|
||
|
||
*Dieser Text muss vor der Veröffentlichung von einer Prüf-Gruppe
|
||
gegengelesen werden. Das ist bei Leichter Sprache üblich.*`,
|
||
|
||
signLanguage: `## Informationen in Deutscher Gebärdensprache
|
||
|
||
Das Video oben erklärt:
|
||
- die wesentlichen Inhalte dieses Portals
|
||
- wie die Navigation funktioniert
|
||
- die wesentlichen Inhalte der Erklärung zur Barrierefreiheit
|
||
|
||
Die Adresse des Videos tragen Sie oben in das Feld ein; es erscheint dann
|
||
über diesem Text.
|
||
|
||
### Kontakt
|
||
|
||
Fragen zur Barrierefreiheit richten Sie bitte an:
|
||
*[Kontaktdaten eintragen]*`,
|
||
}
|
||
|
||
interface SitePagesFormProps {
|
||
open: boolean
|
||
onOpenChange: (open: boolean) => void
|
||
}
|
||
|
||
/**
|
||
* Pflegt Impressum, Datenschutzerklärung und Erklärung zur Barrierefreiheit.
|
||
* Ein leer gelassener Text nimmt die betreffende Seite vom Netz.
|
||
*/
|
||
export function SitePagesForm({ open, onOpenChange }: SitePagesFormProps) {
|
||
const { pages, isLoading } = useSitePages()
|
||
|
||
const [formData, setFormData] = useState<SitePagesContent>(EMPTY_SITE_PAGES)
|
||
const [errors, setErrors] = useState<string[]>([])
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
const [tab, setTab] = useState<SitePageKey>("imprint")
|
||
|
||
// Beim Öffnen den gespeicherten Stand laden
|
||
useEffect(() => {
|
||
if (!open || isLoading) return
|
||
|
||
setErrors([])
|
||
setTab("imprint")
|
||
setFormData({ ...pages })
|
||
// `pages` bewusst nicht als Abhängigkeit: sonst würden Eingaben beim
|
||
// Hintergrund-Refresh von SWR überschrieben.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [open, isLoading])
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault()
|
||
|
||
setErrors([])
|
||
setIsSubmitting(true)
|
||
|
||
try {
|
||
await updateSitePages(formData)
|
||
toast.success("Seiteninhalte gespeichert")
|
||
onOpenChange(false)
|
||
} catch (error) {
|
||
const details =
|
||
error instanceof ApiError && error.details.length > 0
|
||
? error.details
|
||
: [error instanceof Error ? error.message : "Fehler beim Speichern"]
|
||
setErrors(details)
|
||
} finally {
|
||
setIsSubmitting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||
<DialogContent className="max-h-[88vh] sm:max-w-4xl">
|
||
<DialogHeader>
|
||
<DialogTitle>Rechtliche Seiten</DialogTitle>
|
||
<DialogDescription>
|
||
Die eigenständigen Seiten des Portals: drei rechtliche Pflichtseiten
|
||
sowie Leichte Sprache und Gebärdensprache nach § 4 BITV 2.0. Sie
|
||
erscheinen in der Fußzeile der öffentlichen Seiten – ein leer
|
||
gelassener Text nimmt die betreffende Seite vom Netz.
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
|
||
<form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col gap-4">
|
||
<Tabs
|
||
value={tab}
|
||
onValueChange={(value) => setTab(value as SitePageKey)}
|
||
className="min-h-0 flex-1 gap-3"
|
||
>
|
||
<TabsList className="h-auto flex-wrap">
|
||
{SITE_PAGES.map((key) => (
|
||
<TabsTrigger key={key} value={key}>
|
||
{SITE_PAGE_META[key].short}
|
||
</TabsTrigger>
|
||
))}
|
||
</TabsList>
|
||
|
||
{errors.length > 0 && (
|
||
<div className="flex gap-2.5 rounded-lg border border-destructive/30 bg-destructive/5 p-3 text-sm text-destructive">
|
||
<AlertCircle className="mt-0.5 size-4 shrink-0" />
|
||
<ul className="space-y-1">
|
||
{errors.map((error, index) => (
|
||
<li key={index}>{error}</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
|
||
<DialogBody className="pt-1">
|
||
{SITE_PAGES.map((key) => (
|
||
<TabsContent key={key} value={key} className="space-y-3">
|
||
<div className="flex flex-wrap items-center justify-between gap-2 text-sm text-muted-foreground">
|
||
<span>{SITE_PAGE_META[key].hint}</span>
|
||
{formData[key].trim() && (
|
||
<Button variant="ghost" size="sm" asChild>
|
||
<Link href={SITE_PAGE_META[key].path} target="_blank">
|
||
<ExternalLink />
|
||
{SITE_PAGE_META[key].path}
|
||
</Link>
|
||
</Button>
|
||
)}
|
||
</div>
|
||
|
||
{/* Das Video steht in eigenen Feldern, nicht im Markdown:
|
||
Der Markdown-Prozessor filtert HTML weg. */}
|
||
{key === "signLanguage" && (
|
||
<div className="grid gap-3 rounded-lg border bg-muted/30 p-3 sm:grid-cols-2">
|
||
<div className="space-y-2">
|
||
<Label htmlFor="signLanguageVideo" className="gap-1">
|
||
Adresse des Videos
|
||
</Label>
|
||
<Input
|
||
id="signLanguageVideo"
|
||
value={formData.signLanguageVideo}
|
||
onChange={(e) =>
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
signLanguageVideo: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="/videos/gebaerdensprache.mp4"
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
Videodatei auf dem eigenen Server (beginnt mit /) oder
|
||
Player-Adresse eines freigegebenen Anbieters. Derzeit
|
||
freigegeben: alangu.de. Andere Videoportale sind
|
||
bewusst gesperrt – sie übertragen Besucherdaten an
|
||
Dritte.
|
||
</p>
|
||
</div>
|
||
|
||
<div className="space-y-2">
|
||
<Label htmlFor="signLanguageCaptions" className="gap-1">
|
||
Untertiteldatei (optional)
|
||
</Label>
|
||
<Input
|
||
id="signLanguageCaptions"
|
||
value={formData.signLanguageCaptions}
|
||
onChange={(e) =>
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
signLanguageCaptions: e.target.value,
|
||
}))
|
||
}
|
||
placeholder="/videos/gebaerdensprache.vtt"
|
||
/>
|
||
<p className="text-xs text-muted-foreground">
|
||
WebVTT-Datei, nur bei einer eigenen Videodatei nötig
|
||
(WCAG 1.2.2). Ein Player-Anbieter bringt seine
|
||
Untertitel selbst mit.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<MarkdownInput
|
||
id={`site-page-${key}`}
|
||
label={SITE_PAGE_META[key].title}
|
||
value={formData[key]}
|
||
onChange={(value) =>
|
||
setFormData((prev) => ({ ...prev, [key]: value }))
|
||
}
|
||
placeholder={placeholders[key]}
|
||
rows={20}
|
||
/>
|
||
</TabsContent>
|
||
))}
|
||
</DialogBody>
|
||
</Tabs>
|
||
|
||
<DialogFooter className="border-t pt-4">
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
onClick={() => onOpenChange(false)}
|
||
disabled={isSubmitting}
|
||
>
|
||
Abbrechen
|
||
</Button>
|
||
<Button type="submit" disabled={isSubmitting || isLoading}>
|
||
{isSubmitting ? "Speichern…" : "Seiteninhalte speichern"}
|
||
</Button>
|
||
</DialogFooter>
|
||
</form>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
}
|