feat(api,web): eigene Module — Tabelle, API, Seitenleiste, Rahmen-Seite
- Tabelle CustomModule mit Zeilenschutz (tenant_isolation_policy), Migration 20260929120000 - API /custom-modules: Lesen für jeden Angemeldeten, Schreiben nur Administrator, nur https ohne Zugangsdaten - Seitenleiste zeigt eigene Module unter ihrer Kategorie, Rahmen-Seite mit Sandbox und „In neuem Tab öffnen“ - MODULE_CATEGORIES als gemeinsame Liste, Zugriffsklassifikation nachgemessen fortgeschrieben Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { XFRAME_SANDBOX } from '@/components/dashboard/widgets/xframe-config';
|
||||
import { type CustomModule, checkCustomModuleUrl, getCustomModule } from '@/lib/custom-modules-api';
|
||||
|
||||
type LoadState = { status: 'loading' } | { status: 'missing' } | { status: 'ready'; mod: CustomModule };
|
||||
|
||||
/**
|
||||
* Rahmen-Ansicht eines eigenen Moduls (quick-260929-9wc, D-06): schmale
|
||||
* Leiste mit Name, Hinweis und dem immer sichtbaren Knopf „In neuem Tab
|
||||
* oeffnen“, darunter der flaechenfuellende Rahmen. Sandbox-Wert und
|
||||
* Attribute wie im XFrame-Widget (`XFRAME_SANDBOX`, kein `allow`, kein
|
||||
* Referrer). Iframe und Link entstehen NUR bei einer https-Adresse ohne
|
||||
* Zugangsdaten — auch eine von Hand veraenderte Datenbankzeile mit
|
||||
* `javascript:` oder `http:` wird nie gerendert (T-9WC-03).
|
||||
*/
|
||||
export function CustomModuleView({ id }: { id: string }) {
|
||||
const t = useTranslations('customModules');
|
||||
const [state, setState] = useState<LoadState>({ status: 'loading' });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setState({ status: 'loading' });
|
||||
getCustomModule(id)
|
||||
.then((mod) => {
|
||||
if (cancelled) return;
|
||||
setState(mod ? { status: 'ready', mod } : { status: 'missing' });
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setState({ status: 'missing' });
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [id]);
|
||||
|
||||
if (state.status === 'loading') {
|
||||
return <div className="p-4 text-sm text-muted-foreground">{t('loading')}</div>;
|
||||
}
|
||||
if (state.status === 'missing') {
|
||||
return <div className="p-4 text-sm text-muted-foreground">{t('notFound')}</div>;
|
||||
}
|
||||
|
||||
const { mod } = state;
|
||||
const urlOk = checkCustomModuleUrl(mod.url) === 'ok';
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-var(--header-height)-1.5rem)] min-h-[320px] flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h1 className="truncate text-base font-semibold text-foreground">{mod.name}</h1>
|
||||
{urlOk && <p className="truncate text-xs text-muted-foreground">{t('embedHint')}</p>}
|
||||
</div>
|
||||
{urlOk && (
|
||||
<a
|
||||
href={mod.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-secondary shrink-0"
|
||||
>
|
||||
{t('openInNewTab')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
{urlOk ? (
|
||||
<iframe
|
||||
src={mod.url}
|
||||
title={mod.name}
|
||||
sandbox={XFRAME_SANDBOX}
|
||||
allow=""
|
||||
referrerPolicy="no-referrer"
|
||||
className="w-full flex-1 rounded-lg border-0 bg-background"
|
||||
data-testid="custom-module-frame"
|
||||
/>
|
||||
) : (
|
||||
<div className="rounded-md border border-border p-4 text-sm text-muted-foreground">
|
||||
{t('invalidUrl')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user