import { useMutation, useQuery } from "@tanstack/react-query"; import { Download, KeyRound, User, X, Users, Settings as SettingsIcon } from "lucide-react"; import { useState } from "react"; import { useNavigate, useSearchParams } from "react-router"; import { toast } from "sonner"; import { downloadResticPasswordMutation, setRegistrationStatusMutation, getRegistrationStatusOptions, } from "~/client/api-client/@tanstack/react-query.gen"; import { Button } from "~/client/components/ui/button"; import { Card, CardContent, CardDescription, CardTitle } from "~/client/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "~/client/components/ui/dialog"; import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; import { Switch } from "~/client/components/ui/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; import { authClient } from "~/client/lib/auth-client"; import { appContext } from "~/context"; import { TwoFactorSection } from "../components/two-factor-section"; import { UserManagement } from "../components/user-management"; import type { Route } from "./+types/settings"; export const handle = { breadcrumb: () => [{ label: "Settings" }], }; export function meta(_: Route.MetaArgs) { return [ { title: "Zerobyte - Settings" }, { name: "description", content: "Manage your account settings and preferences.", }, ]; } export async function clientLoader({ context }: Route.LoaderArgs) { const ctx = context.get(appContext); return ctx; } 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 [isChangingPassword, setIsChangingPassword] = useState(false); const [searchParams, setSearchParams] = useSearchParams(); const activeTab = searchParams.get("tab") || "account"; const navigate = useNavigate(); const isAdmin = loaderData.user?.role === "admin"; const registrationStatusQuery = useQuery({ ...getRegistrationStatusOptions(), enabled: isAdmin, }); const updateRegistrationStatusMutation = useMutation({ ...setRegistrationStatusMutation(), onSuccess: () => { toast.success("Registration settings updated"); void registrationStatusQuery.refetch(); }, onError: (error) => { toast.error("Failed to update registration settings", { description: error.message, }); }, }); const handleLogout = async () => { await authClient.signOut({ fetchOptions: { onSuccess: () => { void navigate("/login", { replace: true }); }, onError: ({ error }) => { console.error(error); toast.error("Logout failed", { description: error.message }); }, }, }); }; 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 = async (e: React.FormEvent) => { e.preventDefault(); if (newPassword !== confirmPassword) { toast.error("Passwords do not match"); return; } if (newPassword.length < 8) { toast.error("Password must be at least 8 characters long"); return; } await authClient.changePassword({ newPassword, currentPassword: currentPassword, revokeOtherSessions: true, fetchOptions: { onSuccess: () => { toast.success("Password changed successfully. You will be logged out."); setTimeout(() => { void handleLogout(); }, 1500); }, onError: ({ error }) => { toast.error("Failed to change password", { description: error.message, }); }, onRequest: () => { setIsChangingPassword(true); }, onResponse: () => { setIsChangingPassword(false); }, }, }); }; const handleDownloadResticPassword = (e: React.FormEvent) => { e.preventDefault(); if (!downloadPassword) { toast.error("Password is required"); return; } downloadResticPassword.mutate({ body: { password: downloadPassword, }, }); }; const onTabChange = (value: string) => { setSearchParams({ tab: value }); }; return (
Account {isAdmin && Users} {isAdmin && System}
Account Information Your account details
Change Password Update your password to keep your account secure
setCurrentPassword(e.target.value)} className="max-w-md" required />
setNewPassword(e.target.value)} className="max-w-md" required minLength={8} />

Must be at least 8 characters long

setConfirmPassword(e.target.value)} className="max-w-md" required minLength={8} />
Backup Recovery Key Download your recovery key for Restic backups

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.

Download Recovery Key For security reasons, please enter your account password to download the recovery key file.
setDownloadPassword(e.target.value)} placeholder="Enter your password" required />
{isAdmin && (
User Management Manage users, roles and permissions
)} {isAdmin && (
System Settings Manage system-wide settings

When enabled, new users can sign up

updateRegistrationStatusMutation.mutate({ body: { enabled: checked } }) } disabled={registrationStatusQuery.isLoading || updateRegistrationStatusMutation.isPending} />
)}
); }