MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m48s
safeHref lets only http, https and mailto become links (Markdown viewer, outliner, legal links); javascript: and friends show as text. A middleware sets a Content-Security-Policy (scripts only from MindSift, no framing, connections only to this server), X-Frame-Options, nosniff, Referrer-Policy and a Permissions-Policy. The two inline scripts of index.html moved to files so script-src stays 'self'. The second inline script also deleted every cache on each page load, which undermined the offline start; it is gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { AuthStatus } from '../lib/types';
|
|
import { api } from '../lib/api';
|
|
import { MarkdownViewer } from '../lib/markdown';
|
|
import { ReadingModal } from './ReadingModal';
|
|
import { safeHref } from '../lib/safeUrl';
|
|
|
|
type LegalType = 'imprint' | 'privacy';
|
|
const TITLES: Record<LegalType, string> = { imprint: 'Impressum', privacy: 'Datenschutz' };
|
|
|
|
interface LegalLinksProps {
|
|
legal?: AuthStatus['legal'];
|
|
className?: string;
|
|
children?: React.ReactNode; // further footer links, e.g. "Über MindSift"
|
|
}
|
|
|
|
// Impressum / Datenschutz links: an external URL opens in a new tab, Markdown in a reading modal.
|
|
// Links without URL or text are not shown at all.
|
|
export const LegalLinks: React.FC<LegalLinksProps> = ({ legal, className = '', children }) => {
|
|
const [open, setOpen] = useState<{ type: LegalType; content: string } | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const entries = (['imprint', 'privacy'] as LegalType[])
|
|
.map((type) => ({
|
|
type,
|
|
url: type === 'imprint' ? legal?.imprintUrl : legal?.privacyUrl,
|
|
hasContent: type === 'imprint' ? legal?.hasImprintContent : legal?.hasPrivacyContent,
|
|
}))
|
|
.filter((e) => e.url || e.hasContent);
|
|
|
|
if (entries.length === 0 && !children) return null;
|
|
|
|
const openText = async (type: LegalType) => {
|
|
setError(null);
|
|
try {
|
|
setOpen({ type, content: await api.getLegalText(type) });
|
|
} catch (err: any) {
|
|
setError(err.message || 'Text konnte nicht geladen werden');
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-[11px] text-muted ${className}`}>
|
|
{entries.map((e) =>
|
|
e.url && safeHref(e.url) ? (
|
|
<a key={e.type} href={safeHref(e.url) as string} target="_blank" rel="noopener noreferrer" className="hover:text-main underline-offset-2 hover:underline">
|
|
{TITLES[e.type]}
|
|
</a>
|
|
) : (
|
|
<button key={e.type} type="button" onClick={() => openText(e.type)} className="hover:text-main underline-offset-2 hover:underline">
|
|
{TITLES[e.type]}
|
|
</button>
|
|
)
|
|
)}
|
|
{children}
|
|
{error && <span className="text-status-err">{error}</span>}
|
|
{open && (
|
|
<ReadingModal title={TITLES[open.type]} onClose={() => setOpen(null)}>
|
|
<MarkdownViewer content={open.content} />
|
|
</ReadingModal>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|