diff --git a/TESTING.md b/TESTING.md index b83f774..6433512 100644 --- a/TESTING.md +++ b/TESTING.md @@ -1336,6 +1336,26 @@ Kreise plus Rechteck dazwischen, keine Freihandkurve) und in RGBA gerendert. - [ ] Auf dem Startbildschirm frühestens nach Neu-Installieren (Chrome hält eine eigene Kopie) +## Anleitung „Connect as a drive" (24.08.2026) + +Neuer Info-Knopf unter Einstellungen → Access, neben den App-Passwörtern. +Die Adressen entstehen aus `window.location`, nicht aus `PUBLIC_URL` – was im +Browser trägt, trägt auch im Dateimanager. + +Geprüft auf 1280×900 und 412×915: + +- Alle drei Reiter (Windows, Linux · Konsole, Linux · Dateimanager) öffnen. +- Über HTTP erscheint der rote Hinweis und der Dateimanager-Reiter bietet + `dav://` an; über HTTPS `davs://` und kein Hinweis. +- Die Kopierknöpfe stehen an WebDAV-Adresse, Benutzername, Cloud-Adresse und + an jedem Befehlsblock. + +- [ ] Auf der produktiven Instanz gegenprüfen, dass dort `https://…/dav/` + steht und der HTTPS-Hinweis **nicht** erscheint +- [ ] Einen der Befehle wirklich ausführen (davfs2-Einhängen) +- [ ] Windows-Weg auf einem echten Windows durchspielen – die 50-MB-Grenze und + der Registry-Pfad stammen aus der Dokumentation, nicht aus einem Versuch + ## Automatisiert geprüft (22.08.2026) Diese Punkte hat Claude per API/`curl` gegen Wegwerf-Konten durchgespielt; sie diff --git a/frontend/src/components/AppPasswords.tsx b/frontend/src/components/AppPasswords.tsx index 57b2d4a..8df460a 100644 --- a/frontend/src/components/AppPasswords.tsx +++ b/frontend/src/components/AppPasswords.tsx @@ -1,6 +1,8 @@ import { useState, useEffect } from 'react' import { appPasswordsApi, type AppPassword } from '../api/client' import ConfirmDialog from './ConfirmDialog' +import WebDavGuide from './WebDavGuide' +import { InfoIcon } from './icons' /** Zugangsdaten für WebDAV-Clients, die das Hauptpasswort ersetzen: Geht ein * Gerät verloren, widerruft man dieses eine Token statt das Konto neu @@ -12,6 +14,7 @@ export default function AppPasswords({ username }: { username: string }) { const [creating, setCreating] = useState(false) const [error, setError] = useState('') const [revoking, setRevoking] = useState(null) + const [guide, setGuide] = useState(false) // Nur einmal im Klartext vorhanden – danach kennt der Server nur den Hash. const [freshToken, setFreshToken] = useState(null) @@ -67,12 +70,29 @@ export default function AppPasswords({ username }: { username: string }) { return ( <> -

+

WebDAV and sync clients need an app password — your account password does not work there. Each one can be revoked on its own, so a lost device costs you one password, not your account.

+ {/* Die Anleitung steht neben der Erklärung, nicht in ihr: Wer schon weiß, + wie es geht, braucht sie nie – wer sie braucht, sucht sie genau hier, + weil hier das Passwort entsteht. Die WebDAV-Adresse stand vorher + nirgends in der Oberfläche. */} + + {/* Der Klartext erscheint genau einmal; deshalb prominent und mit deutlicher Ansage statt als beiläufige Zeile in der Liste. */} {freshToken && ( @@ -202,6 +222,8 @@ export default function AppPasswords({ username }: { username: string }) { )} + {guide && setGuide(false)} />} + {revoking && ( void +} + +type Tab = 'windows' | 'terminal' | 'files' + +/** Eine Zeile zum Abschreiben – mit Knopf, weil genau das sonst schiefgeht. */ +function CopyRow({ label, value, hint }: { label: string; value: string; hint?: string }) { + const [copied, setCopied] = useState(false) + + function copy() { + navigator.clipboard?.writeText(value).then(() => { + setCopied(true) + setTimeout(() => setCopied(false), 1500) + }) + } + + return ( +
+
{label}
+
+ {value} + +
+ {hint && ( +
+ {hint} +
+ )} +
+ ) +} + +function Step({ n, children }: { n: number; children: React.ReactNode }) { + return ( +
+
{n}
+
{children}
+
+ ) +} + +/** Ein Kasten für das, was an dieser Stelle erfahrungsgemäß schiefgeht. */ +function Caveat({ children }: { children: React.ReactNode }) { + return ( +
+ {children} +
+ ) +} + +function Code({ children }: { children: string }) { + const [copied, setCopied] = useState(false) + return ( +
+
{children}
+ +
+ ) +} + +export default function WebDavGuide({ username, onClose }: Props) { + const overlay = useOverlayDismiss(onClose) + const [tab, setTab] = useState('windows') + + const origin = window.location.origin + const davURL = `${origin}/dav/` + // Dateimanager sprechen nicht https, sondern davs (verschlüsselt) bzw. dav. + const davsURL = davURL.replace(/^https:/, 'davs:').replace(/^http:/, 'dav:') + const isHTTPS = window.location.protocol === 'https:' + const host = window.location.host + // Dolphin kennt beide Schreibweisen; die verschlüsselte heißt anders. + const kioURL = `${isHTTPS ? 'webdavs' : 'webdav'}://${host}/dav/` + + return ( +
+
+

+ Connect as a drive +

+

+ Your files over WebDAV — same folders, in your file manager. Sign in with + your username and an app password; + your account password is refused here on purpose. +

+ + + + + + {!isHTTPS && ( + + This page is not using HTTPS.{' '} + Windows refuses WebDAV over plain HTTP outright, and every other client + sends your app password unencrypted. Put a reverse proxy with TLS in + front before you connect a device. + + )} + +
+ + tabs={[ + { id: 'windows', label: 'Windows' }, + { id: 'terminal', label: 'Linux · terminal' }, + { id: 'files', label: 'Linux · file manager' }, + ]} + active={tab} + onChange={setTab} + /> +
+ + {tab === 'windows' && ( +
+ Open Explorer, right-click This PC → Map network drive… + Folder: {davURL} + Tick Connect using different credentials. + Username {username}, password = your app password. + + + Two things Windows will do to you. +
+ Downloads stop at 50 MB — that is a built-in limit of the + Windows WebDAV client, not of this server. Raise it in + HKLM\SYSTEM\CurrentControlSet\Services\WebClient\Parameters + → FileSizeLimitInBytes (decimal + 4294967295 = 4 GB), then restart the WebClient service. +

+ If mapping fails immediately, that service is usually not running: + services.msc → WebClient → start, + set to Automatic. +

+ For large files, a dedicated client (WinSCP, Cyberduck, RaiDrive) is the + calmer route — none of them have that limit. +
+
+ )} + + {tab === 'terminal' && ( +
+

+ Mount it with davfs2. It asks for username and password + on the first mount. +

+ {`sudo apt install davfs2 # Debian/Ubuntu +# sudo dnf install davfs2 # Fedora +# sudo pacman -S davfs2 # Arch + +sudo mkdir -p /mnt/aether +sudo mount -t davfs ${davURL} /mnt/aether`} + +

+ Without typing the password every time +

+

+ Put the credentials in ~/.davfs2/secrets — the + file must be readable by you alone, davfs2 refuses it otherwise: +

+ {`mkdir -p ~/.davfs2 +echo '${davURL} ${username} YOUR-APP-PASSWORD' >> ~/.davfs2/secrets +chmod 600 ~/.davfs2/secrets`} + +

+ Mount it at boot +

+

+ One line in /etc/fstab: +

+ {`${davURL} /mnt/aether davfs user,noauto,_netdev 0 0`} + + + _netdev matters: without it the + system tries to mount before the network is up and the boot stalls. +

+ davfs2 caches in ~/.davfs2/cache. + A large file is written there first and only then uploaded — so a copy + can look finished while it is still on its way. +

+ Prefer rclone? rclone config → + type webdav → vendor{' '} + other → URL as above. It handles + interrupted transfers better. +
+
+ )} + + {tab === 'files' && ( +
+

+ GNOME Files, Dolphin, Nemo and Thunar all speak WebDAV natively — but they + want their own scheme, not https://: +

+ + + + + GNOME Files: Other Locations at the bottom of the + sidebar → Connect to Server → paste the address → Connect. + + + KDE Dolphin: type the address straight into the location + bar (Ctrl + L). Dolphin also accepts{' '} + {kioURL}. + + + Username {username}, password = your app password. Tick + “remember” so the keyring keeps it. + + + Right-click the mount in the sidebar → Add Bookmark, and it stays + one click away. + + + + These mounts live in GVFS/KIO, not in the kernel — they show up in the file + manager, but not always for other programs. If a tool cannot see the files, + look under /run/user/$(id -u)/gvfs/{' '} + or mount with davfs2 instead (the terminal tab). + +
+ )} + +
+ +
+
+
+ ) +}