feat(nextcloud-files): Modul Dateien mit Nextcloud-Adresse, Verbindungsprüfung und gesicherter Verbindungsschicht

- Migration 20261008180000: NextcloudFilesConfig (Organisation) und NextcloudFilesAccount
  (Mandant UND Benutzer per Zeilenschutz, nur verschlüsseltes App-Passwort)
- Transportschicht ncRequest: feste Pfadanfänge, Segmentcodierung, keine Weiterleitungen,
  keine Cookies, Zeitgrenzen; Aufrufsperre pausiert den Ursprung nach jedem 429 und
  sperrt Zugangsschlüssel nach dem ersten 401
- Einstellungen: Adresse speichern/prüfen, Adresswechsel läuft alle Konten ab (mit Bestätigung)
- Controller mit Verwalten nur auf Einstellungen, Seed, Modul, Registrierung in Web
  (Ladefunktion, Symbol folder, Navigation, Layout) und Reiter Einstellungen
- RLS-Inventar fortgeschrieben, Testaufbau (tessera-nc-test) und E2E-Skripte

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 17:41:46 +02:00
parent 83b842b72c
commit 960696745f
35 changed files with 3282 additions and 2 deletions
@@ -0,0 +1,84 @@
'use client';
import { useTranslations } from 'next-intl';
import { useCallback, useEffect, useState } from 'react';
import { TabBar } from '@/components/accounting/tab-bar';
import { SettingsSection } from '@/components/control-center/settings-section';
import { PageHeader } from '@/components/layout/page-header';
import { getNextcloudFilesStatus, type NextcloudFilesStatus } from '@/lib/nextcloud-files-api';
import { useCanManageModule } from '@/lib/use-module-capability';
import { SettingsTab } from './components/SettingsTab';
type TabId = 'files' | 'settings';
/**
* Dateien (quick-261008-mzu): die Nextcloud-Dateien jedes Benutzers in Tessera.
* Die Adresse der Nextcloud stellen Administratoren und Benutzer mit der
* Freigabestufe Verwalten im Reiter "Einstellungen" ein; alle anderen sehen nur
* den Reiter "Dateien" ohne Reiterleiste — bindend ist allein die API. Der
* Abschnitt `nextcloud-files-main` ist die Stelle, an der das Verbinden des
* eigenen Kontos und der Dateibrowser eingehaengt werden.
*/
export default function NextcloudFilesPage() {
const t = useTranslations('nextcloudFiles');
const canManage = useCanManageModule('nextcloud-files') === true;
const [status, setStatus] = useState<NextcloudFilesStatus | null>(null);
const [statusError, setStatusError] = useState(false);
const [tab, setTab] = useState<TabId>('files');
const reloadStatus = useCallback(async () => {
try {
setStatus(await getNextcloudFilesStatus());
setStatusError(false);
} catch {
setStatusError(true);
}
}, []);
useEffect(() => {
void reloadStatus();
}, [reloadStatus]);
const tabs: { id: TabId; label: string }[] = [{ id: 'files', label: t('tabs.files') }];
if (canManage) tabs.push({ id: 'settings', label: t('tabs.settings') });
const activeTab: TabId = tabs.some((x) => x.id === tab) ? tab : 'files';
return (
<div className="mx-auto max-w-6xl space-y-6 p-3 sm:p-6">
<PageHeader moduleSlug="nextcloud-files" title={t('title')} description={t('description')} />
{statusError && (
<p role="alert" className="text-sm text-destructive">
{t('errors.loadStatus')}
</p>
)}
{canManage && <TabBar tabs={tabs} active={activeTab} onChange={setTab} />}
{activeTab === 'files' && status && !status.configured && (
<SettingsSection title={t('notConfigured.title')}>
<p className="text-sm text-muted-foreground">
{canManage ? t('notConfigured.managerHint') : t('notConfigured.userHint')}
</p>
{canManage && (
<button
type="button"
onClick={() => setTab('settings')}
className="mt-3 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:opacity-90"
>
{t('notConfigured.goToSettings')}
</button>
)}
</SettingsSection>
)}
{activeTab === 'files' && status?.configured && (
<section data-testid="nextcloud-files-main" className="space-y-4">
<p className="text-sm text-muted-foreground">
{t('main.server', { host: status.host ?? '' })}
</p>
</section>
)}
{activeTab === 'settings' && canManage && (
<SettingsTab onChanged={() => void reloadStatus()} />
)}
</div>
);
}