Skip to content

Commit 6b2a7d6

Browse files
Refactors dashboard routes for modularity
Splits profile actions into separate files for delete, revoke session, and update. Enhances profile management by improving the UI and separating concerns. Introduces dashboard route structure to streamline app routing configuration.
1 parent 0b90fc4 commit 6b2a7d6

File tree

6 files changed

+146
-106
lines changed

6 files changed

+146
-106
lines changed

app/features/dashboard/routes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { relative } from "@react-router/dev/routes";
2+
3+
import type { RouteConfig } from "@react-router/dev/routes";
4+
5+
const { route, index } = relative(import.meta.dirname);
6+
7+
export const dashboardRoutes = [
8+
index("./routes/home.tsx"),
9+
route("/profile", "./routes/profile.tsx", [
10+
route("delete", "./routes/profile.delete.tsx"),
11+
route("revoke-session", "./routes/profile.revoke-session.tsx"),
12+
route("update", "./routes/profile.update.tsx"),
13+
]),
14+
] satisfies RouteConfig;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { auth } from "~/lib/auth";
2+
import { createProtectedAction } from "~/lib/secureRoute";
3+
4+
export const action = createProtectedAction({
5+
function: async ({ request }) => {
6+
try {
7+
await auth.api.deleteUser({
8+
headers: request.headers,
9+
body: {
10+
// TODO: Move to env
11+
callbackURL: "http://localhost:5173/goodbye", // Some auth providers require password confirmation
12+
},
13+
});
14+
return { success: true, message: "Account deletion request sent to your email" };
15+
} catch (error) {
16+
return { success: false, error: "Failed to delete account" };
17+
}
18+
},
19+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { z } from "zod";
2+
import { auth } from "~/lib/auth";
3+
import prisma from "~/lib/prismaClient";
4+
import { createProtectedAction } from "~/lib/secureRoute";
5+
6+
export const action = createProtectedAction({
7+
formValidation: z.object({
8+
sessionId: z.string(),
9+
}),
10+
function: async ({ request, form }) => {
11+
if (form.error) {
12+
return { success: false, error: "Failed to revoke session" };
13+
}
14+
const { sessionId } = form.data;
15+
16+
try {
17+
// Fetch the session to get the token through prisma
18+
const sessionFromDb = await prisma.session.findUnique({
19+
where: {
20+
id: sessionId,
21+
},
22+
});
23+
24+
if (!sessionFromDb) {
25+
return { success: false, error: "Session not found" };
26+
}
27+
28+
await auth.api.revokeSession({
29+
headers: request.headers,
30+
body: {
31+
token: sessionFromDb.token,
32+
},
33+
});
34+
return { success: true, message: "Session revoked successfully" };
35+
} catch (error) {
36+
return { success: false, error: "Failed to revoke session" };
37+
}
38+
},
39+
});

app/features/dashboard/routes/profile.tsx

Lines changed: 23 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,10 @@ import { DataTable } from "~/components/data-table";
77
import type { ColumnDef } from "@tanstack/react-table";
88
import { formatDate } from "~/lib/date";
99
import { Badge } from "~/components/ui/badge";
10-
import { useState, useEffect } from "react";
10+
import { useState } from "react";
1111
import { auth } from "~/lib/auth";
12-
import { redirect, useLoaderData, Form, useActionData } from "react-router";
12+
import { useLoaderData, Form } from "react-router";
1313
import { authClient } from "~/lib/auth-client";
14-
import prisma from "~/lib/prismaClient";
1514
import {
1615
AlertDialog,
1716
AlertDialogAction,
@@ -23,8 +22,7 @@ import {
2322
AlertDialogTitle,
2423
} from "~/components/ui/alert-dialog";
2524
import { toast } from "sonner";
26-
import { createProtectedAction, createProtectedLoader } from "~/lib/secureRoute";
27-
import z from "zod";
25+
import { createProtectedLoader } from "~/lib/secureRoute";
2826

2927
type Session = {
3028
id: string;
@@ -33,91 +31,6 @@ type Session = {
3331
isCurrent: boolean;
3432
};
3533

36-
// TODO: split intent into separate actions
37-
export const action = createProtectedAction({
38-
function: async ({ request }) => {
39-
const formData = await request.formData();
40-
const sessionId = formData.get("sessionId") as string;
41-
const intent = formData.get("intent") as string;
42-
const name = formData.get("name") as string;
43-
const email = formData.get("email") as string;
44-
45-
if (intent === "revoke-session" && sessionId) {
46-
try {
47-
// Fetch the session to get the token through prisma
48-
const sessionFromDb = await prisma.session.findUnique({
49-
where: {
50-
id: sessionId,
51-
},
52-
});
53-
54-
if (!sessionFromDb) {
55-
return { success: false, error: "Session not found" };
56-
}
57-
58-
await auth.api.revokeSession({
59-
headers: request.headers,
60-
body: {
61-
token: sessionFromDb.token,
62-
},
63-
});
64-
return { success: true, message: "Session revoked successfully" };
65-
} catch (error) {
66-
return { success: false, error: "Failed to revoke session" };
67-
}
68-
}
69-
70-
if (intent === "update-profile" && name && email) {
71-
try {
72-
const activeSession = await auth.api.getSession({
73-
headers: request.headers,
74-
});
75-
76-
if (!activeSession) {
77-
return redirect("/login");
78-
}
79-
80-
await auth.api.updateUser({
81-
headers: request.headers,
82-
body: {
83-
name,
84-
},
85-
});
86-
87-
if (email !== activeSession.user.email) {
88-
await auth.api.changeEmail({
89-
headers: request.headers,
90-
body: {
91-
newEmail: email,
92-
callbackURL: "http://localhost:5173/app/profile",
93-
},
94-
});
95-
}
96-
97-
return { success: true, message: "Profile updated successfully" };
98-
} catch (error) {
99-
return { success: false, error: "Failed to update profile" };
100-
}
101-
}
102-
103-
if (intent === "delete-account") {
104-
try {
105-
await auth.api.deleteUser({
106-
headers: request.headers,
107-
body: {
108-
callbackURL: "http://localhost:5173/goodbye", // Some auth providers require password confirmation
109-
},
110-
});
111-
return { success: true, message: "Account deletion request sent to your email" };
112-
} catch (error) {
113-
return { success: false, error: "Failed to delete account" };
114-
}
115-
}
116-
117-
return { success: false, error: "Invalid action" };
118-
},
119-
});
120-
12134
export const loader = createProtectedLoader({
12235
function: async ({ request, identity }) => {
12336
const sessions = await auth.api.listSessions({
@@ -147,18 +60,20 @@ export default function ProfilePage() {
14760
const [error, setError] = useState<string | null>(null);
14861
const [sessionToRevoke, setSessionToRevoke] = useState<Session | null>(null);
14962
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
150-
const actionData = useActionData<typeof action>();
63+
// const actionData = useActionData<typeof action>();
64+
65+
/* TODO: show a success toast */
15166

15267
// Show toast notifications when action data changes
153-
useEffect(() => {
154-
if (actionData) {
155-
if (actionData.success) {
156-
toast.success(actionData.message);
157-
} else if (actionData.error) {
158-
toast.error(actionData.error);
159-
}
160-
}
161-
}, [actionData]);
68+
// useEffect(() => {
69+
// if (actionData) {
70+
// if (actionData.success) {
71+
// toast.success(actionData.message);
72+
// } else if (actionData.error) {
73+
// toast.error(actionData.error);
74+
// }
75+
// }
76+
// }, [actionData]);
16277

16378
const columns: ColumnDef<Session>[] = [
16479
{
@@ -217,7 +132,8 @@ export default function ProfilePage() {
217132
</AlertDialogDescription>
218133
</AlertDialogHeader>
219134
<AlertDialogFooter>
220-
<Form method="post">
135+
<Form method="post" action="/app/profile/revoke-session">
136+
{/* TODO: show a success toast */}
221137
<input type="hidden" name="sessionId" value={sessionToRevoke?.id} />
222138
<input type="hidden" name="intent" value="revoke-session" />
223139
<div className="flex gap-2">
@@ -250,7 +166,9 @@ export default function ProfilePage() {
250166
</AlertDialogDescription>
251167
</AlertDialogHeader>
252168
<AlertDialogFooter>
253-
<Form method="post">
169+
<Form method="post" action="/app/profile/delete">
170+
{/* TODO: show a success toast */}
171+
254172
<input type="hidden" name="intent" value="delete-account" />
255173
<div className="flex gap-2">
256174
<AlertDialogCancel asChild>
@@ -283,7 +201,9 @@ export default function ProfilePage() {
283201

284202
<Separator className="my-4" />
285203

286-
<Form method="post">
204+
<Form method="post" action="/app/profile/update">
205+
{/* TODO: show a success toast */}
206+
287207
<input type="hidden" name="intent" value="update-profile" />
288208
<div className="grid gap-4">
289209
<div className="grid gap-2">
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { redirect } from "react-router";
2+
import z from "zod";
3+
import { auth } from "~/lib/auth";
4+
import { createProtectedAction } from "~/lib/secureRoute";
5+
6+
export const action = createProtectedAction({
7+
formValidation: z.object({
8+
name: z.string().min(1, "Name is required"),
9+
email: z.string().email("Invalid email address"),
10+
}),
11+
function: async ({ request, form }) => {
12+
if (form.error) {
13+
return { success: false, error: "Failed to update profile" };
14+
}
15+
const { name, email } = form.data;
16+
17+
try {
18+
const activeSession = await auth.api.getSession({
19+
headers: request.headers,
20+
});
21+
22+
if (!activeSession) {
23+
return redirect("/login");
24+
}
25+
26+
await auth.api.updateUser({
27+
headers: request.headers,
28+
body: {
29+
name,
30+
},
31+
});
32+
33+
if (email !== activeSession.user.email) {
34+
await auth.api.changeEmail({
35+
headers: request.headers,
36+
body: {
37+
newEmail: email,
38+
callbackURL: "http://localhost:5173/app/profile",
39+
},
40+
});
41+
}
42+
43+
return { success: true, message: "Profile updated successfully" };
44+
} catch (error) {
45+
return { success: false, error: "Failed to update profile" };
46+
}
47+
},
48+
});

app/routes.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { adminRoutes } from "./features/admin/routes";
33
import { productsRoutes } from "./features/products/routes";
44
import { authRoutes } from "./features/auth/routes";
55
import { organizationRoutes } from "./features/organization/routes";
6+
import { dashboardRoutes } from "./features/dashboard/routes";
7+
68
export default [
79
index("./features/marketing/routes/landing.tsx"),
810
route("/goodbye", "./features/marketing/routes/goodbye.tsx"),
@@ -15,9 +17,7 @@ export default [
1517
layout("./features/dashboard/layout.tsx", [
1618
...prefix("/app", [
1719
// General routes
18-
index("./features/dashboard/routes/home.tsx"),
19-
route("/profile", "./features/dashboard/routes/profile.tsx"),
20-
20+
...dashboardRoutes,
2121
...organizationRoutes,
2222
...productsRoutes,
2323
...adminRoutes,

0 commit comments

Comments
 (0)