Files
tessera-ctl/apps/web/src/app/(portal)/modules/nextcloud-files/page.tsx
T
schalli e78e059477 fix(nextcloud-files): CR-02 Nextcloud-Adresse aendern nur noch Administratoren
- PUT settings traegt @Roles(ADMIN, SUPER_ADMIN) statt @ModuleManage (Muster wie
  TendersController.getSourceConfig); alle Benutzer schicken ihr Passwort an diese Adresse
- POST settings/test: Verwalter ohne Administratorrolle pruefen nur die gespeicherte Adresse,
  jede andere Adresse ist 403
- Einstellungen fuer Verwalter nur lesbar mit kurzem Hinweis, ohne Speichern-Knopf; Hinweis
  "nicht eingerichtet" verweist Nicht-Administratoren an einen Administrator
- Metadaten-Spec (module-manage-handlers), Controller-Spec, Seitentest, e2e-settings
  (Verwalter: 403 auf PUT und fremde Pruefung), Anleitungen und Changelog angepasst

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 22:31:06 +02:00

149 lines
5.4 KiB
TypeScript

'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,
getServerInfo,
type NextcloudFilesStatus,
type NextcloudServerInfo,
} from '@/lib/nextcloud-files-api';
import { useAuthStore } from '@/lib/stores/auth-store';
import { useCanManageModule } from '@/lib/use-module-capability';
import { AccountBar } from './components/AccountBar';
import { ConnectPanel } from './components/ConnectPanel';
import { FileBrowser } from './components/FileBrowser';
import { SettingsTab } from './components/SettingsTab';
type TabId = 'files' | 'settings';
/**
* Dateien (quick-261008-mzu): die Nextcloud-Dateien jedes Benutzers in Tessera.
* Den Reiter "Einstellungen" sehen Administratoren und Benutzer mit der
* Freigabestufe Verwalten; die Adresse der Nextcloud AENDERN darf nur ein
* Administrator (CR-02). Alle anderen sehen nur den Reiter "Dateien" ohne
* Reiterleiste — bindend ist allein die API. Im
* Abschnitt `nextcloud-files-main` verbindet jeder Benutzer sein eigenes Konto
* (ConnectPanel); ist es verbunden, steht dort die Dateiansicht
* (`nextcloud-files-browser`, FileBrowser). Laeuft die Verbindung ab, laedt die
* Seite den Stand neu und zeigt wieder den Anmeldebildschirm mit Hinweis.
*/
export default function NextcloudFilesPage() {
const t = useTranslations('nextcloudFiles');
const canManage = useCanManageModule('nextcloud-files') === true;
const role = useAuthStore((s) => s.user?.role);
const isAdmin = role === 'ADMIN' || role === 'SUPER_ADMIN';
const [status, setStatus] = useState<NextcloudFilesStatus | null>(null);
const [statusError, setStatusError] = useState(false);
const [tab, setTab] = useState<TabId>('files');
const [serverInfo, setServerInfo] = useState<NextcloudServerInfo | null>(null);
const reloadStatus = useCallback(async () => {
try {
setStatus(await getNextcloudFilesStatus());
setStatusError(false);
} catch {
setStatusError(true);
}
}, []);
useEffect(() => {
void reloadStatus();
}, [reloadStatus]);
// Name, Farbe und Logo der Nextcloud fuer Anmeldebildschirm und Kontoleiste. Ein Fehler ist
// harmlos: ohne Kennung steht der Rechnername da.
const configured = status?.configured === true;
const serverUrl = status?.serverUrl ?? null;
// biome-ignore lint/correctness/useExhaustiveDependencies: eine neue Adresse laedt die Kennung neu
useEffect(() => {
if (!configured) {
setServerInfo(null);
return;
}
let cancelled = false;
getServerInfo()
.then((info) => {
if (!cancelled) setServerInfo(info);
})
.catch(() => {
if (!cancelled) setServerInfo(null);
});
return () => {
cancelled = true;
};
}, [configured, serverUrl]);
const account = status?.account ?? null;
const connected = account?.connected === true;
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')}
actions={
activeTab === 'files' && status?.configured && account && connected ? (
<AccountBar
account={account}
host={status.host}
info={serverInfo}
onDisconnected={reloadStatus}
/>
) : undefined
}
/>
{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">
{isAdmin ? t('notConfigured.managerHint') : t('notConfigured.userHint')}
</p>
{isAdmin && 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">
{connected ? (
<div data-testid="nextcloud-files-browser">
<FileBrowser serverUrl={status.serverUrl} onExpired={() => void reloadStatus()} />
</div>
) : (
<ConnectPanel
host={status.host}
info={serverInfo}
expired={account?.expired === true}
onConnected={reloadStatus}
/>
)}
</section>
)}
{activeTab === 'settings' && canManage && (
<SettingsTab onChanged={() => void reloadStatus()} />
)}
</div>
);
}