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 { t } = useTranslation();
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
|
const [editPass, setEditPass] = useState(false);
|
||||||
|
const [newPass, setNewPass] = useState("");
|
||||||
const fullUrl = window.location.origin + link.url;
|
const fullUrl = window.location.origin + link.url;
|
||||||
|
|
||||||
const copy = async () => {
|
const copy = async () => {
|
||||||
@@ -106,6 +108,16 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
|
|||||||
mutationFn: () => api.revokeDownloadLink(link.id),
|
mutationFn: () => api.revokeDownloadLink(link.id),
|
||||||
onSuccess: onChanged,
|
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[] = [];
|
const badges: string[] = [];
|
||||||
if (link.has_password) badges.push(t("downloads.share.hasPassword"));
|
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" />}
|
{copied ? <Check className="w-4 h-4 text-emerald-400" /> : <Copy className="w-4 h-4" />}
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (
|
if (
|
||||||
@@ -165,6 +194,39 @@ function LinkRow({ link, onChanged }: { link: ShareLink; onChanged: () => void }
|
|||||||
{t("downloads.share.allowDownload")}
|
{t("downloads.share.allowDownload")}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,7 +118,12 @@
|
|||||||
"views": "{{n}} views",
|
"views": "{{n}} views",
|
||||||
"expiresOn": "expires {{date}}",
|
"expiresOn": "expires {{date}}",
|
||||||
"hasPassword": "password",
|
"hasPassword": "password",
|
||||||
"downloadable": "downloadable"
|
"downloadable": "downloadable",
|
||||||
|
"setPassword": "Set a password",
|
||||||
|
"changePassword": "Change password",
|
||||||
|
"newPassword": "New password",
|
||||||
|
"savePassword": "Save",
|
||||||
|
"removePassword": "Remove"
|
||||||
},
|
},
|
||||||
"confirm": {
|
"confirm": {
|
||||||
"cancelTitle": "Cancel download?",
|
"cancelTitle": "Cancel download?",
|
||||||
|
|||||||
@@ -118,7 +118,12 @@
|
|||||||
"views": "{{n}} megtekintés",
|
"views": "{{n}} megtekintés",
|
||||||
"expiresOn": "lejár: {{date}}",
|
"expiresOn": "lejár: {{date}}",
|
||||||
"hasPassword": "jelszó",
|
"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": {
|
"confirm": {
|
||||||
"cancelTitle": "Megszakítod a letöltést?",
|
"cancelTitle": "Megszakítod a letöltést?",
|
||||||
|
|||||||
Reference in New Issue
Block a user