feat: download recovery file restic password

This commit is contained in:
Nicolas Meienberger
2025-11-08 17:52:43 +01:00
parent b289920720
commit b5ba03da3d
23 changed files with 957 additions and 27 deletions

View File

@@ -1,11 +1,24 @@
import { useMutation } from "@tanstack/react-query";
import { KeyRound, User } from "lucide-react";
import { Download, KeyRound, User } from "lucide-react";
import { useState } from "react";
import { useNavigate } from "react-router";
import { toast } from "sonner";
import { changePasswordMutation, logoutMutation } from "~/api-client/@tanstack/react-query.gen";
import {
changePasswordMutation,
downloadResticPasswordMutation,
logoutMutation,
} from "~/api-client/@tanstack/react-query.gen";
import { Button } from "~/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
import { Card, CardContent, CardDescription, CardTitle } from "~/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "~/components/ui/dialog";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import { appContext } from "~/context";
@@ -30,6 +43,8 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
const [downloadPassword, setDownloadPassword] = useState("");
const navigate = useNavigate();
const logout = useMutation({
@@ -56,6 +71,28 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
},
});
const downloadResticPassword = useMutation({
...downloadResticPasswordMutation(),
onSuccess: (data) => {
const blob = new Blob([data], { type: "text/plain" });
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "restic.pass";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
toast.success("Restic password file downloaded successfully");
setDownloadDialogOpen(false);
setDownloadPassword("");
},
onError: (error) => {
toast.error("Failed to download Restic password", { description: error.message });
},
});
const handleChangePassword = (e: React.FormEvent) => {
e.preventDefault();
@@ -77,6 +114,21 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
});
};
const handleDownloadResticPassword = (e: React.FormEvent) => {
e.preventDefault();
if (!downloadPassword) {
toast.error("Password is required");
return;
}
downloadResticPassword.mutate({
body: {
password: downloadPassword,
},
});
};
return (
<Card className="p-0 gap-0">
<div className="border-b border-border/50 bg-card-header p-6">
@@ -143,6 +195,69 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
</Button>
</form>
</CardContent>
<div className="border-t border-border/50 bg-card-header p-6">
<CardTitle className="flex items-center gap-2">
<Download className="size-5" />
Backup Recovery Key
</CardTitle>
<CardDescription className="mt-1.5">Download your Restic password file for disaster recovery</CardDescription>
</div>
<CardContent className="p-6 space-y-4">
<p className="text-sm text-muted-foreground max-w-2xl">
This file contains the encryption password used by Restic to secure your backups. Store it in a safe place
(like a password manager or encrypted storage). If you lose access to this server, you'll need this file to
recover your backup data.
</p>
<Dialog open={downloadDialogOpen} onOpenChange={setDownloadDialogOpen}>
<DialogTrigger asChild>
<Button variant="outline">
<Download size={16} className="mr-2" />
Download Restic Password
</Button>
</DialogTrigger>
<DialogContent>
<form onSubmit={handleDownloadResticPassword}>
<DialogHeader>
<DialogTitle>Download Restic Password</DialogTitle>
<DialogDescription>
For security reasons, please enter your account password to download the Restic password file.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="download-password">Your Password</Label>
<Input
id="download-password"
type="password"
value={downloadPassword}
onChange={(e) => setDownloadPassword(e.target.value)}
placeholder="Enter your password"
required
autoFocus
/>
</div>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => {
setDownloadDialogOpen(false);
setDownloadPassword("");
}}
>
Cancel
</Button>
<Button type="submit" loading={downloadResticPassword.isPending}>
Download
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</CardContent>
</Card>
);
}