Skip to content

Commit af77d16

Browse files
Refactors organization management routes
Splits organization route into separate update and delete routes, removing the old unified general route. Updates routes' structure to improve modularity and better aligns with RESTful practices. Integrates redirection logic for protected loaders. Simplifies profile-related protected actions with added loaders redirecting to profile page. Enhances maintainability by organizing routes logically. Fixes #123
1 parent 6b2a7d6 commit af77d16

File tree

7 files changed

+168
-72
lines changed

7 files changed

+168
-72
lines changed

app/features/dashboard/routes/profile.delete.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { redirect } from "react-router";
12
import { auth } from "~/lib/auth";
2-
import { createProtectedAction } from "~/lib/secureRoute";
3+
import { createProtectedAction, createProtectedLoader } from "~/lib/secureRoute";
34

45
export const action = createProtectedAction({
56
function: async ({ request }) => {
@@ -17,3 +18,9 @@ export const action = createProtectedAction({
1718
}
1819
},
1920
});
21+
22+
export const loader = createProtectedLoader({
23+
function: async () => {
24+
return redirect("/app/profile");
25+
},
26+
});

app/features/dashboard/routes/profile.revoke-session.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { redirect } from "react-router";
12
import { z } from "zod";
23
import { auth } from "~/lib/auth";
34
import prisma from "~/lib/prismaClient";
4-
import { createProtectedAction } from "~/lib/secureRoute";
5+
import { createProtectedAction, createProtectedLoader } from "~/lib/secureRoute";
56

67
export const action = createProtectedAction({
78
formValidation: z.object({
@@ -37,3 +38,9 @@ export const action = createProtectedAction({
3738
}
3839
},
3940
});
41+
42+
export const loader = createProtectedLoader({
43+
function: async () => {
44+
return redirect("/app/profile");
45+
},
46+
});

app/features/dashboard/routes/profile.update.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { redirect } from "react-router";
22
import z from "zod";
33
import { auth } from "~/lib/auth";
4-
import { createProtectedAction } from "~/lib/secureRoute";
4+
import { createProtectedAction, createProtectedLoader } from "~/lib/secureRoute";
55

66
export const action = createProtectedAction({
77
formValidation: z.object({
@@ -46,3 +46,9 @@ export const action = createProtectedAction({
4646
}
4747
},
4848
});
49+
50+
export const loader = createProtectedLoader({
51+
function: async () => {
52+
return redirect("/app/profile");
53+
},
54+
});

app/features/organization/routes.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ export const organizationRoutes = [
1212
route("join", "./routes/onboarding.join.tsx"),
1313
]),
1414
route("/onboarding/join/:inviteId", "./routes/onboarding.join.byId.tsx"),
15-
route("/", "./routes/general.tsx"),
15+
route("/", "./routes/organization.tsx", [
16+
route("edit", "./routes/organization.update.tsx"),
17+
route("delete", "./routes/organization.delete.tsx"),
18+
]),
1619
route("/members", "./routes/members.tsx", [
1720
route(":memberId/remove", "./routes/members.remove.tsx"),
1821
route(":memberId/set-role", "./routes/members.setRole.tsx"),
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { redirect } from "react-router";
2+
import z from "zod";
3+
import { auth } from "~/lib/auth";
4+
import { createProtectedAction, createProtectedLoader } from "~/lib/secureRoute";
5+
6+
export const action = createProtectedAction({
7+
permissions: {
8+
action: "delete",
9+
subject: "Organization",
10+
},
11+
formValidation: z.object({
12+
organizationId: z.string(),
13+
}),
14+
function: async ({ request, form }) => {
15+
if (form.error) {
16+
return { success: false, error: "Failed to update organization" };
17+
}
18+
const { organizationId } = form.data;
19+
20+
if (typeof auth.api.deleteOrganization !== "function") {
21+
return { error: "Organization delete API not implemented" };
22+
}
23+
await auth.api.deleteOrganization({
24+
headers: request.headers,
25+
body: {
26+
organizationId,
27+
},
28+
});
29+
return redirect("/app/organization/select");
30+
},
31+
});
32+
33+
export const loader = createProtectedLoader({
34+
function: async () => {
35+
return redirect("/app/organization");
36+
},
37+
});

app/features/organization/routes/general.tsx renamed to app/features/organization/routes/organization.tsx

Lines changed: 59 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import {
2-
useLoaderData,
3-
useActionData,
4-
useNavigation,
5-
Form,
6-
redirect,
7-
type ActionFunctionArgs,
8-
} from "react-router";
1+
import { useLoaderData, useNavigation, Form, type ActionFunctionArgs } from "react-router";
92
import { z } from "zod";
103
import { InputWithLabel } from "~/components/input-with-label";
114
import { Button } from "~/components/ui/button";
@@ -27,64 +20,60 @@ import {
2720
} from "~/components/ui/alert-dialog";
2821
import { useState } from "react";
2922
import { auth } from "~/lib/auth";
30-
import { ensureCanWithIdentity } from "~/lib/permissions.server";
31-
import { getUserInformation } from "~/lib/identity.server";
23+
3224
import { createProtectedLoader } from "~/lib/secureRoute";
3325

34-
const orgNameSchema = z.object({
35-
name: z.string().min(1, "Name is required"),
36-
});
26+
// const orgNameSchema = z.object({
27+
// name: z.string().min(1, "Name is required"),
28+
// });
3729

3830
// TODO: Move to secureRoute, split up into multiple actions
3931
export async function action({ request }: ActionFunctionArgs) {
40-
const identity = await getUserInformation(request);
41-
42-
const formData = await request.formData();
43-
const intent = formData.get("intent");
44-
const name = formData.get("name") as string | undefined;
45-
const organizationId = formData.get("organizationId") as string;
46-
47-
if (intent === "edit") {
48-
ensureCanWithIdentity(identity, "update", "Organization");
49-
50-
try {
51-
const validated = orgNameSchema.parse({ name });
52-
// TODO: Replace with actual updateOrganization API if available
53-
if (typeof auth.api.updateOrganization !== "function") {
54-
return { error: "Organization update API not implemented" };
55-
}
56-
await auth.api.updateOrganization({
57-
headers: request.headers,
58-
body: {
59-
organizationId,
60-
data: {
61-
name: validated.name,
62-
},
63-
},
64-
});
65-
return { success: true };
66-
} catch (error) {
67-
if (error instanceof z.ZodError) {
68-
return { errors: error.flatten().fieldErrors };
69-
}
70-
return { error: "Failed to update organization" };
71-
}
72-
}
73-
74-
if (intent === "delete") {
75-
ensureCanWithIdentity(identity, "delete", "Organization");
76-
77-
if (typeof auth.api.deleteOrganization !== "function") {
78-
return { error: "Organization delete API not implemented" };
79-
}
80-
await auth.api.deleteOrganization({
81-
headers: request.headers,
82-
body: {
83-
organizationId,
84-
},
85-
});
86-
return redirect("/app/organization/select");
87-
}
32+
// const identity = await getUserInformation(request);
33+
34+
// const formData = await request.formData();
35+
// const intent = formData.get("intent");
36+
// // const name = formData.get("name") as string | undefined;
37+
// const organizationId = formData.get("organizationId") as string;
38+
39+
// if (intent === "edit") {
40+
// ensureCanWithIdentity(identity, "update", "Organization");
41+
42+
// try {
43+
// const validated = orgNameSchema.parse({ name });
44+
45+
// await auth.api.updateOrganization({
46+
// headers: request.headers,
47+
// body: {
48+
// organizationId,
49+
// data: {
50+
// name: validated.name,
51+
// },
52+
// },
53+
// });
54+
// return { success: true };
55+
// } catch (error) {
56+
// if (error instanceof z.ZodError) {
57+
// return { errors: error.flatten().fieldErrors };
58+
// }
59+
// return { error: "Failed to update organization" };
60+
// }
61+
// }
62+
63+
// if (intent === "delete") {
64+
// ensureCanWithIdentity(identity, "delete", "Organization");
65+
66+
// if (typeof auth.api.deleteOrganization !== "function") {
67+
// return { error: "Organization delete API not implemented" };
68+
// }
69+
// await auth.api.deleteOrganization({
70+
// headers: request.headers,
71+
// body: {
72+
// organizationId,
73+
// },
74+
// });
75+
// return redirect("/app/organization/select");
76+
// }
8877

8978
return null;
9079
}
@@ -107,7 +96,6 @@ export const loader = createProtectedLoader({
10796

10897
export default function OrganizationGeneral() {
10998
const { activeOrg } = useLoaderData<{ activeOrg: { id: string; name: string; slug: string } }>();
110-
const actionData = useActionData<typeof action>();
11199
const navigation = useNavigation();
112100
const isSubmitting = navigation.state === "submitting";
113101
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
@@ -119,14 +107,15 @@ export default function OrganizationGeneral() {
119107
<CardTitle>Organization Details</CardTitle>
120108
<CardDescription>Manage your organization settings.</CardDescription>
121109
</CardHeader>
122-
<Form method="post" autoComplete="off">
110+
<Form method="post" action="/app/organization/edit" autoComplete="off">
111+
{/* TODO: add error feedback from action */}
123112
<CardContent className="space-y-4 py-4">
124113
<InputWithLabel
125114
label="Name"
126115
id="name"
127116
name="name"
128117
defaultValue={activeOrg.name}
129-
error={actionData?.errors?.name?.[0]}
118+
// error={actionData?.errors?.name?.[0]}
130119
autoFocus
131120
/>
132121
<input type="hidden" name="organizationId" value={activeOrg.id} />
@@ -136,10 +125,12 @@ export default function OrganizationGeneral() {
136125
<Button type="submit" disabled={isSubmitting} className="w-full">
137126
{isSubmitting ? "Saving..." : "Save Changes"}
138127
</Button>
139-
{actionData?.error && (
128+
{/* TODO: add error feedback from action */}
129+
130+
{/* {actionData?.error && (
140131
<div className="text-destructive text-sm text-center">{actionData.error}</div>
141-
)}
142-
{actionData?.success && <div className="text-success text-sm text-center">Saved!</div>}
132+
)} */}
133+
{/* {actionData?.success && <div className="text-success text-sm text-center">Saved!</div>} */}
143134
</CardFooter>
144135
</Form>
145136
</Card>
@@ -172,7 +163,7 @@ export default function OrganizationGeneral() {
172163
undone.
173164
</AlertDialogDescription>
174165
</AlertDialogHeader>
175-
<Form method="post">
166+
<Form method="post" action="/app/organization/delete">
176167
<input type="hidden" name="organizationId" value={activeOrg.id} />
177168
<input type="hidden" name="intent" value="delete" />
178169
<AlertDialogFooter>
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { redirect } from "react-router";
2+
import z from "zod";
3+
import { auth } from "~/lib/auth";
4+
import { createProtectedAction, createProtectedLoader } from "~/lib/secureRoute";
5+
6+
export const action = createProtectedAction({
7+
permissions: {
8+
action: "update",
9+
subject: "Organization",
10+
},
11+
formValidation: z.object({
12+
name: z.string().min(1, "Name is required"),
13+
organizationId: z.string(),
14+
}),
15+
function: async ({ request, form }) => {
16+
if (form.error) {
17+
return { success: false, error: "Failed to update organization" };
18+
}
19+
const { name, organizationId } = form.data;
20+
21+
try {
22+
await auth.api.updateOrganization({
23+
headers: request.headers,
24+
body: {
25+
organizationId,
26+
data: {
27+
name: name,
28+
},
29+
},
30+
});
31+
return { success: true };
32+
} catch (error) {
33+
if (error instanceof z.ZodError) {
34+
return { errors: error.flatten().fieldErrors };
35+
}
36+
return { error: "Failed to update organization" };
37+
}
38+
},
39+
});
40+
41+
export const loader = createProtectedLoader({
42+
function: async () => {
43+
return redirect("/app/organization");
44+
},
45+
});

0 commit comments

Comments
 (0)