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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
Reference in New Issue
Block a user