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 { 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,
@ -24,31 +23,20 @@ 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 { type AppContext } from "~/context";
import { TwoFactorSection } from "../components/two-factor-section";
import { UserManagement } from "../components/user-management";
import type { Route } from "./+types/settings";
import { useNavigate, useSearch } from "@tanstack/react-router";
export const handle = {
breadcrumb: () => [{ label: "Settings" }],
};
export function meta(_: Route.MetaArgs) {
return [
{ title: "Zerobyte - Settings" },
{
name: "description",
content: "Manage your account settings and preferences.",
},
];
}
type Props = {
appContext: AppContext;
};
export async function clientLoader({ context }: Route.LoaderArgs) {
const ctx = context.get(appContext);
return ctx;
}
export default function Settings({ loaderData }: Route.ComponentProps) {
export function SettingsPage({ appContext }: Props) {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
@ -56,11 +44,11 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
const [downloadPassword, setDownloadPassword] = useState("");
const [isChangingPassword, setIsChangingPassword] = useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const activeTab = searchParams.get("tab") || "account";
const { tab } = useSearch({ from: "/(dashboard)/settings/" });
const activeTab = tab || "account";
const navigate = useNavigate();
const isAdmin = loaderData.user?.role === "admin";
const isAdmin = appContext.user?.role === "admin";
const registrationStatusQuery = useQuery({
...getRegistrationStatusOptions(),
@ -84,7 +72,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
await authClient.signOut({
fetchOptions: {
onSuccess: () => {
void navigate("/login", { replace: true });
void navigate({ to: "/login", replace: true });
},
onError: ({ 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();
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();
if (!downloadPassword) {
@ -173,7 +161,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
};
const onTabChange = (value: string) => {
setSearchParams({ tab: value });
navigate({ to: ".", search: () => ({ tab: value }) });
};
return (
@ -198,7 +186,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
<CardContent className="p-6 space-y-4">
<div className="space-y-2">
<Label>Username</Label>
<Input value={loaderData.user?.username || ""} disabled className="max-w-md" />
<Input value={appContext.user?.username || ""} disabled className="max-w-md" />
</div>
</CardContent>
@ -318,7 +306,7 @@ export default function Settings({ loaderData }: Route.ComponentProps) {
</Dialog>
</CardContent>
<TwoFactorSection twoFactorEnabled={loaderData.user?.twoFactorEnabled} />
<TwoFactorSection twoFactorEnabled={appContext.user?.twoFactorEnabled} />
</Card>
</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 authDownloadRecoveryKeyRouteImport } from './routes/(auth)/download-recovery-key'
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 dashboardNotificationsIndexRouteImport } from './routes/(dashboard)/notifications/index'
import { Route as dashboardBackupsIndexRouteImport } from './routes/(dashboard)/backups/index'
@ -59,6 +60,11 @@ const dashboardVolumesIndexRoute = dashboardVolumesIndexRouteImport.update({
path: '/volumes/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardSettingsIndexRoute = dashboardSettingsIndexRouteImport.update({
id: '/settings/',
path: '/settings/',
getParentRoute: () => dashboardRouteRoute,
} as any)
const dashboardRepositoriesIndexRoute =
dashboardRepositoriesIndexRouteImport.update({
id: '/repositories/',
@ -157,6 +163,7 @@ export interface FileRoutesByFullPath {
'/backups/': typeof dashboardBackupsIndexRoute
'/notifications/': typeof dashboardNotificationsIndexRoute
'/repositories/': typeof dashboardRepositoriesIndexRoute
'/settings/': typeof dashboardSettingsIndexRoute
'/volumes/': typeof dashboardVolumesIndexRoute
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
@ -178,6 +185,7 @@ export interface FileRoutesByTo {
'/backups': typeof dashboardBackupsIndexRoute
'/notifications': typeof dashboardNotificationsIndexRoute
'/repositories': typeof dashboardRepositoriesIndexRoute
'/settings': typeof dashboardSettingsIndexRoute
'/volumes': typeof dashboardVolumesIndexRoute
'/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
'/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
@ -201,6 +209,7 @@ export interface FileRoutesById {
'/(dashboard)/backups/': typeof dashboardBackupsIndexRoute
'/(dashboard)/notifications/': typeof dashboardNotificationsIndexRoute
'/(dashboard)/repositories/': typeof dashboardRepositoriesIndexRoute
'/(dashboard)/settings/': typeof dashboardSettingsIndexRoute
'/(dashboard)/volumes/': typeof dashboardVolumesIndexRoute
'/(dashboard)/repositories/$repoId/$snapshotId': typeof dashboardRepositoriesRepoIdSnapshotIdRoute
'/(dashboard)/backups/$backupId/$snapshotId/restore': typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
@ -224,6 +233,7 @@ export interface FileRouteTypes {
| '/backups/'
| '/notifications/'
| '/repositories/'
| '/settings/'
| '/volumes/'
| '/repositories/$repoId/$snapshotId'
| '/backups/$backupId/$snapshotId/restore'
@ -245,6 +255,7 @@ export interface FileRouteTypes {
| '/backups'
| '/notifications'
| '/repositories'
| '/settings'
| '/volumes'
| '/repositories/$repoId/$snapshotId'
| '/backups/$backupId/$snapshotId/restore'
@ -267,6 +278,7 @@ export interface FileRouteTypes {
| '/(dashboard)/backups/'
| '/(dashboard)/notifications/'
| '/(dashboard)/repositories/'
| '/(dashboard)/settings/'
| '/(dashboard)/volumes/'
| '/(dashboard)/repositories/$repoId/$snapshotId'
| '/(dashboard)/backups/$backupId/$snapshotId/restore'
@ -325,6 +337,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof dashboardVolumesIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/settings/': {
id: '/(dashboard)/settings/'
path: '/settings'
fullPath: '/settings/'
preLoaderRoute: typeof dashboardSettingsIndexRouteImport
parentRoute: typeof dashboardRouteRoute
}
'/(dashboard)/repositories/': {
id: '/(dashboard)/repositories/'
path: '/repositories'
@ -438,6 +457,7 @@ interface dashboardRouteRouteChildren {
dashboardBackupsIndexRoute: typeof dashboardBackupsIndexRoute
dashboardNotificationsIndexRoute: typeof dashboardNotificationsIndexRoute
dashboardRepositoriesIndexRoute: typeof dashboardRepositoriesIndexRoute
dashboardSettingsIndexRoute: typeof dashboardSettingsIndexRoute
dashboardVolumesIndexRoute: typeof dashboardVolumesIndexRoute
dashboardRepositoriesRepoIdSnapshotIdRoute: typeof dashboardRepositoriesRepoIdSnapshotIdRoute
dashboardBackupsBackupIdSnapshotIdRestoreRoute: typeof dashboardBackupsBackupIdSnapshotIdRestoreRoute
@ -458,6 +478,7 @@ const dashboardRouteRouteChildren: dashboardRouteRouteChildren = {
dashboardBackupsIndexRoute: dashboardBackupsIndexRoute,
dashboardNotificationsIndexRoute: dashboardNotificationsIndexRoute,
dashboardRepositoriesIndexRoute: dashboardRepositoriesIndexRoute,
dashboardSettingsIndexRoute: dashboardSettingsIndexRoute,
dashboardVolumesIndexRoute: dashboardVolumesIndexRoute,
dashboardRepositoriesRepoIdSnapshotIdRoute:
dashboardRepositoriesRepoIdSnapshotIdRoute,

View file

@ -6,7 +6,7 @@ import type { AppContext } from "~/context";
import { authMiddleware } from "~/middleware/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 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} />;
}