feat(nextcloud-files): Anmeldung per Passwort und im Browser (Zwei-Faktor), Abmelden mit Widerruf

- Anmelde-Client (getapppassword, cloud/user, Widerruf, Login Flow v2 mit fester Abfrageadresse,
  Link aus Basis und Token neu gebaut), Anmeldebremse 3/15 min je Benutzer und 8/30 min je Server,
  Ablaufspeicher für Browser-Anmeldungen (20 min, höchstens 200, eine je Benutzer)
- Kontodienst: Verbinden, Trennen mit Widerruf, Sitzung mit Zugangsschlüssel-Sperre,
  frisch ausgestellte oder ersetzte App-Passwörter bleiben nie verwaist; jeder Kontozugriff
  über forTenant mit Mandant UND Benutzer aus dem Token
- Migration 20261008183000: Spalte ncLoginName (App-Passwort gilt nur für den Anmeldenamen der
  Ausstellung, gemessen mit E-Mail-Anmeldung gegen Nextcloud 34)
- Verbindungsbildschirm und Kontoleiste, Texte de/en, RLS-Inventar fortgeschrieben,
  E2E-Skript e2e-connect.sh

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 18:02:45 +02:00
parent 960696745f
commit d00b6ff79f
25 changed files with 3533 additions and 55 deletions
@@ -7,6 +7,8 @@ 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 { AccountBar } from './components/AccountBar';
import { ConnectPanel } from './components/ConnectPanel';
import { SettingsTab } from './components/SettingsTab';
type TabId = 'files' | 'settings';
@@ -15,9 +17,10 @@ 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.
* 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 der Dateibereich
* (`nextcloud-files-browser`, den die Dateiansicht fuellt).
*/
export default function NextcloudFilesPage() {
const t = useTranslations('nextcloudFiles');
@@ -40,13 +43,25 @@ export default function NextcloudFilesPage() {
void reloadStatus();
}, [reloadStatus]);
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')} />
<PageHeader
moduleSlug="nextcloud-files"
title={t('title')}
description={t('description')}
actions={
activeTab === 'files' && status?.configured && account && connected ? (
<AccountBar account={account} host={status.host} onDisconnected={reloadStatus} />
) : undefined
}
/>
{statusError && (
<p role="alert" className="text-sm text-destructive">
{t('errors.loadStatus')}
@@ -71,9 +86,19 @@ export default function NextcloudFilesPage() {
)}
{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>
{connected ? (
<div data-testid="nextcloud-files-browser">
<p className="text-sm text-muted-foreground">
{t('main.server', { host: status.host ?? '' })}
</p>
</div>
) : (
<ConnectPanel
host={status.host}
expired={account?.expired === true}
onConnected={reloadStatus}
/>
)}
</section>
)}
{activeTab === 'settings' && canManage && (