refactor: settings

This commit is contained in:
Nicolas Meienberger 2026-02-08 15:54:02 +01:00
parent 975ff23799
commit 1f1d584eea
4 changed files with 56 additions and 28 deletions

View file

@ -1,7 +1,6 @@
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import { Download, KeyRound, User, X, Users, Settings as SettingsIcon } from "lucide-react"; import { Download, KeyRound, User, X, Users, Settings as SettingsIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import { useNavigate, useSearchParams } from "react-router";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
downloadResticPasswordMutation, downloadResticPasswordMutation,
@ -24,31 +23,20 @@ import { Label } from "~/client/components/ui/label";
import { Switch } from "~/client/components/ui/switch"; import { Switch } from "~/client/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
import { authClient } from "~/client/lib/auth-client"; import { authClient } from "~/client/lib/auth-client";
import { appContext } from "~/context"; import { type AppContext } from "~/context";
import { TwoFactorSection } from "../components/two-factor-section"; import { TwoFactorSection } from "../components/two-factor-section";
import { UserManagement } from "../components/user-management"; import { UserManagement } from "../components/user-management";
import type { Route } from "./+types/settings"; import { useNavigate, useSearch } from "@tanstack/react-router";
export const handle = { export const handle = {
breadcrumb: () => [{ label: "Settings" }], breadcrumb: () => [{ label: "Settings" }],
}; };
export function meta(_: Route.MetaArgs) { type Props = {
return [ appContext: AppContext;
{ title: "Zerobyte - Settings" }, };
{
name: "description",
content: "Manage your account settings and preferences.",
},
];
}
export async function clientLoader({ context }: Route.LoaderArgs) { export function SettingsPage({ appContext }: Props) {
const ctx = context.get(appContext);
return ctx;
}
export default function Settings({ loaderData }: Route.ComponentProps) {
const [currentPassword, setCurrentPassword] = useState(""); const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState(""); const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState("");
@ -56,11 +44,11 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
const [downloadPassword, setDownloadPassword] = useState(""); const [downloadPassword, setDownloadPassword] = useState("");
const [isChangingPassword, setIsChangingPassword] = useState(false); const [isChangingPassword, setIsChangingPassword] = useState(false);
const [searchParams, setSearchParams] = useSearchParams(); const { tab } = useSearch({ from: "/(dashboard)/settings/" });
const activeTab = searchParams.get("tab") || "account"; const activeTab = tab || "account";
const navigate = useNavigate(); const navigate = useNavigate();
const isAdmin = loaderData.user?.role === "admin"; const isAdmin = appContext.user?.role === "admin";
const registrationStatusQuery = useQuery({ const registrationStatusQuery = useQuery({
...getRegistrationStatusOptions(), ...getRegistrationStatusOptions(),
@ -84,7 +72,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
await authClient.signOut({ await authClient.signOut({
fetchOptions: { fetchOptions: {
onSuccess: () => { onSuccess: () => {
void navigate("/login", { replace: true }); void navigate({ to: "/login", replace: true });
}, },
onError: ({ error }) => { onError: ({ error }) => {
console.error(error); console.error(error);
@ -118,7 +106,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
}, },
}); });
const handleChangePassword = async (e: React.FormEvent) => { const handleChangePassword = async (e: React.SubmitEvent) => {
e.preventDefault(); e.preventDefault();
if (newPassword !== confirmPassword) { if (newPassword !== confirmPassword) {
@ -157,7 +145,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
}); });
}; };
const handleDownloadResticPassword = (e: React.FormEvent) => { const handleDownloadResticPassword = (e: React.SubmitEvent) => {
e.preventDefault(); e.preventDefault();
if (!downloadPassword) { if (!downloadPassword) {
@ -173,7 +161,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
}; };
const onTabChange = (value: string) => { const onTabChange = (value: string) => {
setSearchParams({ tab: value }); navigate({ to: ".", search: () => ({ tab: value }) });
}; };
return ( return (
@ -198,7 +186,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
<CardContent className="p-6 space-y-4"> <CardContent className="p-6 space-y-4">
<div className="space-y-2"> <div className="space-y-2">
<Label>Username</Label> <Label>Username</Label>
<Input value={loaderData.user?.username || ""} disabled className="max-w-md" /> <Input value={appContext.user?.username || ""} disabled className="max-w-md" />
</div> </div>
</CardContent> </CardContent>
@ -318,7 +306,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
</Dialog> </Dialog>
</CardContent> </CardContent>
<TwoFactorSection twoFactorEnabled={loaderData.user?.twoFactorEnabled} /> <TwoFactorSection twoFactorEnabled={appContext.user?.twoFactorEnabled} />
</Card> </Card>
</TabsContent> </TabsContent>

View file

@ -15,6 +15,7 @@ import { Route as authOnboardingRouteImport } from './routes/(auth)/onboarding'
import { Route as authLoginRouteImport } from './routes/(auth)/login' import { Route as authLoginRouteImport } from './routes/(auth)/login'
import { Route as authDownloadRecoveryKeyRouteImport } from './routes/(auth)/download-recovery-key' import { Route as authDownloadRecoveryKeyRouteImport } from './routes/(auth)/download-recovery-key'
import { Route as dashboardVolumesIndexRouteImport } from './routes/(dashboard)/volumes/index' import { Route as dashboardVolumesIndexRouteImport } from './routes/(dashboard)/volumes/index'
import { Route as dashboardSettingsIndexRouteImport } from './routes/(dashboard)/settings/index'
import { Route as dashboardRepositoriesIndexRouteImport } from './routes/(dashboard)/repositories/index' import { Route as dashboardRepositoriesIndexRouteImport } from './routes/(dashboard)/repositories/index'
import { Route as dashboardNotificationsIndexRouteImport } from './routes/(dashboard)/notifications/index' import { Route as dashboardNotificationsIndexRouteImport } from './routes/(dashboard)/notifications/index'
import { Route as dashboardBackupsIndexRouteImport } from './routes/(dashboard)/backups/index' import { Route as dashboardBackupsIndexRouteImport } from './routes/(dashboard)/backups/index'
@ -59,6 +60,11 @@ const dashboardVolumesIndexRoute = dashboardVolumesIndexRouteImport.update({
path: '/volumes/', path: '/volumes/',
getParentRoute: () => dashboardRouteRoute, getParentRoute: () => dashboardRouteRoute,
} as any) } as any)
const dashboardSettingsIndexRoute = dashboardSettingsIndexRouteImport.update({
id: '/settings/',
path: '/settings/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardRepositoriesIndexRoute = const dashboardRepositoriesIndexRoute =
dashboardRepositoriesIndexRouteImport.update({ dashboardRepositoriesIndexRouteImport.update({
id: '/repositories/', id: '/repositories/',
@ -157,6 +163,7 @@ export interface FileRoutesByFullPath {
'/backups/': typeof dashboardBackupsIndexRoute '/backups/': typeof dashboardBackupsIndexRoute
'/notifications/': typeof dashboardNotificationsIndexRoute '/notifications/': typeof dashboardNotificationsIndexRoute
'/repositories/': typeof dashboardRepositoriesIndexRoute '/repositories/': typeof dashboardRepositoriesIndexRoute
'/settings/': typeof dashboardSettingsIndexRoute
'/volumes/': typeof dashboardVolumesIndexRoute '/volumes/': typeof dashboardVolumesIndexRoute
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute '/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute '/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
@ -178,6 +185,7 @@ export interface FileRoutesByTo {
'/backups': typeof dashboardBackupsIndexRoute '/backups': typeof dashboardBackupsIndexRoute
'/notifications': typeof dashboardNotificationsIndexRoute '/notifications': typeof dashboardNotificationsIndexRoute
'/repositories': typeof dashboardRepositoriesIndexRoute '/repositories': typeof dashboardRepositoriesIndexRoute
'/settings': typeof dashboardSettingsIndexRoute
'/volumes': typeof dashboardVolumesIndexRoute '/volumes': typeof dashboardVolumesIndexRoute
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute '/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute '/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
@ -201,6 +209,7 @@ export interface FileRoutesById {
'/(dashboard)/backups/': typeof dashboardBackupsIndexRoute '/(dashboard)/backups/': typeof dashboardBackupsIndexRoute
'/(dashboard)/notifications/': typeof dashboardNotificationsIndexRoute '/(dashboard)/notifications/': typeof dashboardNotificationsIndexRoute
'/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute '/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute
'/(dashboard)/settings/': typeof dashboardSettingsIndexRoute
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute '/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
'/(dashboard)/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute '/(dashboard)/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
'/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute '/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
@ -224,6 +233,7 @@ export interface FileRouteTypes {
| '/backups/' | '/backups/'
| '/notifications/' | '/notifications/'
| '/repositories/' | '/repositories/'
| '/settings/'
| '/volumes/' | '/volumes/'
| '/repositories/$repoId/$snapshotId' | '/repositories/$repoId/$snapshotId'
| '/backups/$backupId/$snapshotId/restore' | '/backups/$backupId/$snapshotId/restore'
@ -245,6 +255,7 @@ export interface FileRouteTypes {
| '/backups' | '/backups'
| '/notifications' | '/notifications'
| '/repositories' | '/repositories'
| '/settings'
| '/volumes' | '/volumes'
| '/repositories/$repoId/$snapshotId' | '/repositories/$repoId/$snapshotId'
| '/backups/$backupId/$snapshotId/restore' | '/backups/$backupId/$snapshotId/restore'
@ -267,6 +278,7 @@ export interface FileRouteTypes {
| '/(dashboard)/backups/' | '/(dashboard)/backups/'
| '/(dashboard)/notifications/' | '/(dashboard)/notifications/'
| '/(dashboard)/repositories/' | '/(dashboard)/repositories/'
| '/(dashboard)/settings/'
| '/(dashboard)/volumes/' | '/(dashboard)/volumes/'
| '/(dashboard)/repositories/$repoId/$snapshotId' | '/(dashboard)/repositories/$repoId/$snapshotId'
| '/(dashboard)/backups/$backupId/$snapshotId/restore' | '/(dashboard)/backups/$backupId/$snapshotId/restore'
@ -325,6 +337,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardVolumesIndexRouteImport preLoaderRoute: typeof dashboardVolumesIndexRouteImport
parentRoute: typeof dashboardRouteRoute parentRoute: typeof dashboardRouteRoute
} }
'/(dashboard)/settings/': {
id: '/(dashboard)/settings/'
path: '/settings'
fullPath: '/settings/'
preLoaderRoute: typeof dashboardSettingsIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/repositories/': { '/(dashboard)/repositories/': {
id: '/(dashboard)/repositories/' id: '/(dashboard)/repositories/'
path: '/repositories' path: '/repositories'
@ -438,6 +457,7 @@ interface dashboardRouteRouteChildren {
dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute
dashboardNotificationsIndexRoute: typeof dashboardNotificationsIndexRoute dashboardNotificationsIndexRoute: typeof dashboardNotificationsIndexRoute
dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute
dashboardSettingsIndexRoute: typeof dashboardSettingsIndexRoute
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute
dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
@ -458,6 +478,7 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardBackupsIndexRoute: dashboardBackupsIndexRoute, dashboardBackupsIndexRoute: dashboardBackupsIndexRoute,
dashboardNotificationsIndexRoute: dashboardNotificationsIndexRoute, dashboardNotificationsIndexRoute: dashboardNotificationsIndexRoute,
dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute, dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute,
dashboardSettingsIndexRoute: dashboardSettingsIndexRoute,
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute, dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
dashboardRepositoriesRepoIdSnapshotIdRoute: dashboardRepositoriesRepoIdSnapshotIdRoute:
dashboardRepositoriesRepoIdSnapshotIdRoute, dashboardRepositoriesRepoIdSnapshotIdRoute,

View file

@ -6,7 +6,7 @@ import type { AppContext } from "~/context";
import { authMiddleware } from "~/middleware/auth"; import { authMiddleware } from "~/middleware/auth";
import { auth } from "~/server/lib/auth"; import { auth } from "~/server/lib/auth";
const fetchUser = createServerFn({ method: "GET" }).handler(async () => { export const fetchUser = createServerFn({ method: "GET" }).handler(async () => {
const headers = getRequestHeaders(); const headers = getRequestHeaders();
const session = await auth.api.getSession({ headers }); const session = await auth.api.getSession({ headers });

View file

@ -0,0 +1,19 @@
import { createFileRoute } from "@tanstack/react-router";
import { fetchUser } from "../route";
import type { AppContext } from "~/context";
import { SettingsPage } from "~/client/modules/settings/routes/settings";
import { type } from "arktype";
export const Route = createFileRoute("/(dashboard)/settings/")({
component: RouteComponent,
validateSearch: type({ tab: "string?" }),
loader: async () => {
const authContext = await fetchUser();
return authContext as AppContext;
},
});
function RouteComponent() {
const appContext = Route.useLoaderData();
return <SettingsPage appContext={appContext} />;
}