feat(webdav): Anleitung zum Einhaengen neben den App-Passwoertern
Ein Info-Knopf unter Einstellungen -> Access oeffnet drei Wege: Windows (Netzlaufwerk), Linux ueber davfs2 samt fstab und secrets, und Linux ueber den Dateimanager. Dazu WebDAV-Adresse, Benutzername und Cloud-Adresse zum Kopieren. Die WebDAV-Adresse stand bisher nirgends in der Oberflaeche - sie ist zwar ableitbar, aber nur wenn man weiss, dass es sie gibt. Sie entsteht aus window.location statt aus PUBLIC_URL: Was im Browser traegt, traegt auch im Dateimanager. Mitgenommen sind die drei Stellen, an denen es sonst scheitert: Windows bricht ohne HTTPS ab und laedt nichts ueber 50 MB, die Konsole braucht davfs2, und Dateimanager wollen davs:// statt https://. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9a2f90c89b
commit
0ef7bd67ab
3 files changed
+364
-1
No files matched your search
+20
@@ -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
|
||||
|
||||
@@ -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<AppPassword | null>(null)
|
||||
const [guide, setGuide] = useState(false)
|
||||
|
||||
// Nur einmal im Klartext vorhanden – danach kennt der Server nur den Hash.
|
||||
const [freshToken, setFreshToken] = useState<string | null>(null)
|
||||
@@ -67,12 +70,29 @@ export default function AppPasswords({ username }: { username: string }) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<p style={{ margin: '-8px 0 16px', fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.5 }}>
|
||||
<p style={{ margin: '-8px 0 12px', fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.5 }}>
|
||||
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.
|
||||
</p>
|
||||
|
||||
{/* 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. */}
|
||||
<button
|
||||
onClick={() => setGuide(true)}
|
||||
style={{
|
||||
display: 'inline-flex', alignItems: 'center', gap: 6, marginBottom: 16,
|
||||
padding: '6px 12px', borderRadius: 8,
|
||||
border: '1px solid var(--color-border)', background: 'transparent',
|
||||
color: 'var(--color-text)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<InfoIcon size={14} />
|
||||
How to connect a drive
|
||||
</button>
|
||||
|
||||
{/* 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 }) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{guide && <WebDavGuide username={username} onClose={() => setGuide(false)} />}
|
||||
|
||||
{revoking && (
|
||||
<ConfirmDialog
|
||||
title="Revoke app password"
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
import { useState } from 'react'
|
||||
import { useOverlayDismiss } from '../hooks/useOverlayDismiss'
|
||||
import Tabs from './Tabs'
|
||||
|
||||
/**
|
||||
* Wie hänge ich diese Cloud als Laufwerk ein?
|
||||
*
|
||||
* Die Adresse steht bisher nirgends. Sie ist zwar ableitbar – Cloud-Adresse
|
||||
* plus `/dav/` –, aber „ableitbar" heißt: Man muss wissen, dass es sie gibt.
|
||||
* Und die drei Wege dorthin (Windows, Konsole, Dateimanager) haben je eine
|
||||
* Eigenheit, an der es sonst scheitert:
|
||||
*
|
||||
* * Windows verlangt HTTPS und lädt ohne Registry-Eingriff nichts über 50 MB.
|
||||
* * Die Konsole braucht ein zusätzliches Paket (davfs2).
|
||||
* * Dateimanager wollen ein eigenes Schema: `davs://` statt `https://`.
|
||||
*
|
||||
* Die Adressen entstehen aus `window.location`, nicht aus einer Einstellung:
|
||||
* Was im Browser funktioniert, funktioniert auch im Dateimanager – eine
|
||||
* falsch gesetzte PUBLIC_URL würde hier sonst eine Adresse anbieten, die
|
||||
* nachweislich nicht die ist, über die der Betrachter gerade hereinkam.
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
username: string
|
||||
onClose: () => 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 (
|
||||
<div style={{ marginBottom: 10 }}>
|
||||
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginBottom: 4 }}>{label}</div>
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
||||
<code style={{
|
||||
flex: 1, minWidth: 0, padding: '8px 10px', borderRadius: 8,
|
||||
background: 'var(--color-surface-2)', color: 'var(--color-text)',
|
||||
fontSize: 12.5, fontFamily: 'monospace',
|
||||
overflowX: 'auto', whiteSpace: 'nowrap', userSelect: 'all',
|
||||
}}>{value}</code>
|
||||
<button
|
||||
onClick={copy}
|
||||
style={{
|
||||
padding: '8px 12px', borderRadius: 8, border: '1px solid var(--color-border)',
|
||||
background: 'transparent', color: 'var(--color-text)', flexShrink: 0,
|
||||
fontSize: 11.5, fontWeight: 600, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
{hint && (
|
||||
<div style={{ fontSize: 11, color: 'var(--color-text-muted)', marginTop: 4, lineHeight: 1.5 }}>
|
||||
{hint}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Step({ n, children }: { n: number; children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', gap: 10, marginBottom: 9 }}>
|
||||
<div style={{
|
||||
width: 19, height: 19, borderRadius: '50%', flexShrink: 0, marginTop: 1,
|
||||
background: 'var(--color-surface-2)', color: 'var(--color-text-muted)',
|
||||
fontSize: 11, fontWeight: 700,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>{n}</div>
|
||||
<div style={{ fontSize: 13, color: 'var(--color-text)', lineHeight: 1.55 }}>{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** Ein Kasten für das, was an dieser Stelle erfahrungsgemäß schiefgeht. */
|
||||
function Caveat({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div style={{
|
||||
marginTop: 12, padding: '10px 12px', borderRadius: 9,
|
||||
border: '1px solid var(--color-border)',
|
||||
background: 'var(--color-surface-2)',
|
||||
fontSize: 12, color: 'var(--color-text-muted)', lineHeight: 1.6,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Code({ children }: { children: string }) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
return (
|
||||
<div style={{ position: 'relative', marginBottom: 10 }}>
|
||||
<pre style={{
|
||||
margin: 0, padding: '10px 12px', paddingRight: 62, borderRadius: 8,
|
||||
background: 'var(--color-surface-2)', color: 'var(--color-text)',
|
||||
fontSize: 12, lineHeight: 1.65, overflowX: 'auto',
|
||||
fontFamily: 'monospace',
|
||||
}}>{children}</pre>
|
||||
<button
|
||||
onClick={() => {
|
||||
navigator.clipboard?.writeText(children).then(() => {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute', top: 6, right: 6,
|
||||
padding: '4px 9px', borderRadius: 6, border: '1px solid var(--color-border)',
|
||||
background: 'var(--color-surface)', color: 'var(--color-text-muted)',
|
||||
fontSize: 10.5, fontWeight: 600, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
{copied ? 'Copied' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function WebDavGuide({ username, onClose }: Props) {
|
||||
const overlay = useOverlayDismiss(onClose)
|
||||
const [tab, setTab] = useState<Tab>('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 (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
style={{ background: 'rgba(0,0,0,0.6)' }}
|
||||
{...overlay}>
|
||||
<div className="w-full rounded-xl shadow-2xl border"
|
||||
style={{
|
||||
maxWidth: 620, maxHeight: '86vh', overflowY: 'auto',
|
||||
padding: 20,
|
||||
background: 'var(--color-surface)', borderColor: 'var(--color-border)',
|
||||
}}>
|
||||
<h2 className="font-semibold mb-1" style={{ color: 'var(--color-text)' }}>
|
||||
Connect as a drive
|
||||
</h2>
|
||||
<p style={{ margin: '0 0 16px', fontSize: 12.5, color: 'var(--color-text-muted)', lineHeight: 1.55 }}>
|
||||
Your files over WebDAV — same folders, in your file manager. Sign in with
|
||||
your username and an <strong style={{ color: 'var(--color-text)' }}>app password</strong>;
|
||||
your account password is refused here on purpose.
|
||||
</p>
|
||||
|
||||
<CopyRow label="WebDAV address" value={davURL} />
|
||||
<CopyRow label="Username" value={username} />
|
||||
<CopyRow
|
||||
label="Cloud address (browser)"
|
||||
value={origin}
|
||||
hint="The normal address — for sharing links or opening the cloud on another device."
|
||||
/>
|
||||
|
||||
{!isHTTPS && (
|
||||
<Caveat>
|
||||
<strong style={{ color: 'var(--color-danger)' }}>This page is not using HTTPS.</strong>{' '}
|
||||
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.
|
||||
</Caveat>
|
||||
)}
|
||||
|
||||
<div style={{ marginTop: 18 }}>
|
||||
<Tabs<Tab>
|
||||
tabs={[
|
||||
{ id: 'windows', label: 'Windows' },
|
||||
{ id: 'terminal', label: 'Linux · terminal' },
|
||||
{ id: 'files', label: 'Linux · file manager' },
|
||||
]}
|
||||
active={tab}
|
||||
onChange={setTab}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{tab === 'windows' && (
|
||||
<div>
|
||||
<Step n={1}>Open Explorer, right-click <strong>This PC</strong> → <strong>Map network drive…</strong></Step>
|
||||
<Step n={2}>Folder: <code style={{ fontSize: 12 }}>{davURL}</code></Step>
|
||||
<Step n={3}>Tick <strong>Connect using different credentials</strong>.</Step>
|
||||
<Step n={4}>Username <strong>{username}</strong>, password = your app password.</Step>
|
||||
|
||||
<Caveat>
|
||||
<strong style={{ color: 'var(--color-text)' }}>Two things Windows will do to you.</strong>
|
||||
<br />
|
||||
Downloads stop at <strong>50 MB</strong> — that is a built-in limit of the
|
||||
Windows WebDAV client, not of this server. Raise it in
|
||||
<code style={{ fontSize: 11.5 }}> HKLM\SYSTEM\CurrentControlSet\Services\WebClient\Parameters </code>
|
||||
→ <code style={{ fontSize: 11.5 }}>FileSizeLimitInBytes</code> (decimal
|
||||
4294967295 = 4 GB), then restart the <em>WebClient</em> service.
|
||||
<br /><br />
|
||||
If mapping fails immediately, that service is usually not running:
|
||||
<code style={{ fontSize: 11.5 }}> services.msc </code>→ WebClient → start,
|
||||
set to Automatic.
|
||||
<br /><br />
|
||||
For large files, a dedicated client (WinSCP, Cyberduck, RaiDrive) is the
|
||||
calmer route — none of them have that limit.
|
||||
</Caveat>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'terminal' && (
|
||||
<div>
|
||||
<p style={{ margin: '0 0 10px', fontSize: 13, color: 'var(--color-text)', lineHeight: 1.55 }}>
|
||||
Mount it with <strong>davfs2</strong>. It asks for username and password
|
||||
on the first mount.
|
||||
</p>
|
||||
<Code>{`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`}</Code>
|
||||
|
||||
<p style={{ margin: '14px 0 8px', fontSize: 13, fontWeight: 600, color: 'var(--color-text)' }}>
|
||||
Without typing the password every time
|
||||
</p>
|
||||
<p style={{ margin: '0 0 8px', fontSize: 12.5, color: 'var(--color-text-muted)', lineHeight: 1.55 }}>
|
||||
Put the credentials in <code style={{ fontSize: 12 }}>~/.davfs2/secrets</code> — the
|
||||
file must be readable by you alone, davfs2 refuses it otherwise:
|
||||
</p>
|
||||
<Code>{`mkdir -p ~/.davfs2
|
||||
echo '${davURL} ${username} YOUR-APP-PASSWORD' >> ~/.davfs2/secrets
|
||||
chmod 600 ~/.davfs2/secrets`}</Code>
|
||||
|
||||
<p style={{ margin: '14px 0 8px', fontSize: 13, fontWeight: 600, color: 'var(--color-text)' }}>
|
||||
Mount it at boot
|
||||
</p>
|
||||
<p style={{ margin: '0 0 8px', fontSize: 12.5, color: 'var(--color-text-muted)', lineHeight: 1.55 }}>
|
||||
One line in <code style={{ fontSize: 12 }}>/etc/fstab</code>:
|
||||
</p>
|
||||
<Code>{`${davURL} /mnt/aether davfs user,noauto,_netdev 0 0`}</Code>
|
||||
|
||||
<Caveat>
|
||||
<code style={{ fontSize: 11.5 }}>_netdev</code> matters: without it the
|
||||
system tries to mount before the network is up and the boot stalls.
|
||||
<br /><br />
|
||||
davfs2 caches in <code style={{ fontSize: 11.5 }}>~/.davfs2/cache</code>.
|
||||
A large file is written there first and only then uploaded — so a copy
|
||||
can look finished while it is still on its way.
|
||||
<br /><br />
|
||||
Prefer <strong>rclone</strong>? <code style={{ fontSize: 11.5 }}>rclone config</code> →
|
||||
type <code style={{ fontSize: 11.5 }}>webdav</code> → vendor{' '}
|
||||
<code style={{ fontSize: 11.5 }}>other</code> → URL as above. It handles
|
||||
interrupted transfers better.
|
||||
</Caveat>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'files' && (
|
||||
<div>
|
||||
<p style={{ margin: '0 0 12px', fontSize: 13, color: 'var(--color-text)', lineHeight: 1.55 }}>
|
||||
GNOME Files, Dolphin, Nemo and Thunar all speak WebDAV natively — but they
|
||||
want their own scheme, not <code style={{ fontSize: 12 }}>https://</code>:
|
||||
</p>
|
||||
|
||||
<CopyRow
|
||||
label="Address for the file manager"
|
||||
value={davsURL}
|
||||
hint={isHTTPS
|
||||
? 'davs:// means WebDAV over TLS. Plain dav:// would send your password in the clear.'
|
||||
: 'dav:// (unencrypted) because this page is not on HTTPS — see the warning above.'}
|
||||
/>
|
||||
|
||||
<Step n={1}>
|
||||
<strong>GNOME Files:</strong> <em>Other Locations</em> at the bottom of the
|
||||
sidebar → <em>Connect to Server</em> → paste the address → Connect.
|
||||
</Step>
|
||||
<Step n={2}>
|
||||
<strong>KDE Dolphin:</strong> type the address straight into the location
|
||||
bar (Ctrl + L). Dolphin also accepts{' '}
|
||||
<code style={{ fontSize: 12 }}>{kioURL}</code>.
|
||||
</Step>
|
||||
<Step n={3}>
|
||||
Username <strong>{username}</strong>, password = your app password. Tick
|
||||
“remember” so the keyring keeps it.
|
||||
</Step>
|
||||
<Step n={4}>
|
||||
Right-click the mount in the sidebar → <em>Add Bookmark</em>, and it stays
|
||||
one click away.
|
||||
</Step>
|
||||
|
||||
<Caveat>
|
||||
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 <code style={{ fontSize: 11.5 }}>/run/user/$(id -u)/gvfs/</code>{' '}
|
||||
or mount with davfs2 instead (the terminal tab).
|
||||
</Caveat>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 18 }}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
padding: '8px 18px', borderRadius: 8, border: 'none',
|
||||
background: 'var(--color-primary)', color: '#fff',
|
||||
fontSize: 13, fontWeight: 600, cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user