feat(downloads): change/clear a share link's password from the UI

C-3.7 made a password rotation invalidate outstanding grants, but ShareDialog
only let you set a password at link creation — there was no way to rotate or
clear it afterward, so the protection wasn't reachable. Add a lock control to
each existing link row: set / change / remove the password via the existing
update_link endpoint (which bumps password_version and revokes old grants).
This commit is contained in:
2026-07-23 04:03:11 +02:00
parent e49b77dbcd
commit c8cbbb6189
3 changed files with 74 additions and 2 deletions
+62
View File
@@ -88,6 +88,8 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
const { t } = useTranslation();
const confirm = useConfirm();
const [copied, setCopied] = useState(false);
const [editPass, setEditPass] = useState(false);
const [newPass, setNewPass] = useState("");
const fullUrl = window.location.origin + link.url;
const copy = async () => {
@@ -106,6 +108,16 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
mutationFn: () => api.revokeDownloadLink(link.id),
onSuccess: onChanged,
});
// Set / change / clear the link password. Rotating (or clearing) it bumps the link's
// password_version server-side, which invalidates any watch grant issued under the old password.
const savePass = useMutation({
mutationFn: (pw: string) => api.updateDownloadLink(link.id, { password: pw }),
onSuccess: () => {
setEditPass(false);
setNewPass("");
onChanged();
},
});
const badges: string[] = [];
if (link.has_password) badges.push(t("downloads.share.hasPassword"));
@@ -131,6 +143,23 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
>
{copied ? <Check className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />}
</button>
<button
onClick={() => {
setNewPass("");
setEditPass((v) => !v);
}}
title={
link.has_password
? t("downloads.share.changePassword")
: t("downloads.share.setPassword")
}
className={clsx(
"p-1.5 rounded-md hover:bg-surface transition",
link.has_password ? "text-accent" : "text-muted hover:text-fg",
)}
>
<Lock className="w-4 h-4" />
</button>
<button
onClick={async () => {
if (
@@ -165,6 +194,39 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
{t("downloads.share.allowDownload")}
</label>
</div>
{editPass && (
<div className="flex items-center gap-2">
<input
type="password"
autoComplete="new-password"
value={newPass}
onChange={(e) => setNewPass(e.target.value)}
onKeyDown={(e) =>
e.key === "Enter" && newPass.trim() && savePass.mutate(newPass.trim())
}
placeholder={t(
link.has_password ? "downloads.share.newPassword" : "downloads.share.password",
)}
className="flex-1 min-w-0 bg-card border border-border rounded-md px-2 py-1 text-xs outline-none focus:border-accent"
/>
<button
onClick={() => savePass.mutate(newPass.trim())}
disabled={savePass.isPending || !newPass.trim()}
className="px-2 py-1 rounded-md text-xs font-semibold bg-accent text-accent-fg hover:opacity-90 disabled:opacity-40 transition"
>
{t("downloads.share.savePassword")}
</button>
{link.has_password && (
<button
onClick={() => savePass.mutate("")}
disabled={savePass.isPending}
className="px-2 py-1 rounded-md text-xs text-muted hover:text-red-400 transition"
>
{t("downloads.share.removePassword")}
</button>
)}
</div>
)}
</div>
);
}
+6 -1
View File
@@ -118,7 +118,12 @@
"views": "{{n}} views",
"expiresOn": "expires {{date}}",
"hasPassword": "password",
"downloadable": "downloadable"
"downloadable": "downloadable",
"setPassword": "Set a password",
"changePassword": "Change password",
"newPassword": "New password",
"savePassword": "Save",
"removePassword": "Remove"
},
"confirm": {
"cancelTitle": "Cancel download?",
+6 -1
View File
@@ -118,7 +118,12 @@
"views": "{{n}} megtekintés",
"expiresOn": "lejár: {{date}}",
"hasPassword": "jelszó",
"downloadable": "letölthető"
"downloadable": "letölthető",
"setPassword": "Jelszó beállítása",
"changePassword": "Jelszó módosítása",
"newPassword": "Új jelszó",
"savePassword": "Mentés",
"removePassword": "Törlés"
},
"confirm": {
"cancelTitle": "Megszakítod a letöltést?",