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
+45
View File
@@ -12,11 +12,25 @@ const BASE = '/modules/nextcloud-files';
export interface NextcloudFilesAccount {
connected: boolean;
expired: boolean;
status: 'ACTIVE' | 'EXPIRED';
ncUserId: string | null;
displayName: string | null;
connectedVia: 'PASSWORD' | 'LOGIN_FLOW' | null;
connectedAt: string | null;
}
export interface NextcloudFilesFlowStart {
flowId: string;
/** Link zur Anmeldeseite der Nextcloud; der Benutzer oeffnet ihn mit eigenem Klick. */
loginUrl: string;
expiresAt: string;
}
export type NextcloudFilesFlowPoll =
| { state: 'pending' }
| { state: 'connected' }
| { state: 'failed'; code: string; message: string };
export interface NextcloudFilesStatus {
configured: boolean;
serverUrl: string | null;
@@ -118,3 +132,34 @@ export function saveNextcloudFilesSettings(input: {
export function testNextcloudFilesSettings(baseUrl: string): Promise<NextcloudFilesCheck> {
return request<NextcloudFilesCheck>('/settings/test', { method: 'POST', json: { baseUrl } });
}
/**
* Verbinden mit Benutzername und Passwort. Das Passwort geht genau einmal an die
* API und wird nirgends zwischengespeichert; die Antwort enthaelt kein Geheimnis.
*/
export function connectWithPassword(input: {
loginName: string;
password: string;
}): Promise<NextcloudFilesStatus> {
return request<NextcloudFilesStatus>('/connect/password', { method: 'POST', json: input });
}
/** Startet die Browser-Anmeldung (Login Flow v2) fuer Konten mit Zwei-Faktor-Anmeldung. */
export function startLoginFlow(): Promise<NextcloudFilesFlowStart> {
return request<NextcloudFilesFlowStart>('/connect/flow', { method: 'POST' });
}
export function pollLoginFlow(flowId: string): Promise<NextcloudFilesFlowPoll> {
return request<NextcloudFilesFlowPoll>(`/connect/flow/${encodeURIComponent(flowId)}`);
}
export function cancelLoginFlow(flowId: string): Promise<{ cancelled: true }> {
return request<{ cancelled: true }>(`/connect/flow/${encodeURIComponent(flowId)}`, {
method: 'DELETE',
});
}
/** Trennt die Verbindung; die API widerruft den Zugang bei Nextcloud. */
export function disconnectNextcloud(): Promise<{ disconnected: true }> {
return request<{ disconnected: true }>('/connect', { method: 'DELETE' });
}