Skip to content

Commit b65259c

Browse files
feat: add custom auth options to ecosystem settings
1 parent e67a300 commit b65259c

File tree

4 files changed

+233
-80
lines changed

4 files changed

+233
-80
lines changed

apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/auth-options-form.client.tsx

Lines changed: 207 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,21 @@ import { Checkbox, CheckboxWithLabel } from "@/components/ui/checkbox";
44
import { Skeleton } from "@/components/ui/skeleton";
55
import { cn } from "@/lib/utils";
66
import { useState } from "react";
7+
import { useFieldArray, useForm } from "react-hook-form";
78
import { toast } from "sonner";
89
import invariant from "tiny-invariant";
10+
import { Button } from "../../../../../../../../../@/components/ui/button";
11+
import { Card } from "../../../../../../../../../@/components/ui/card";
12+
import {
13+
Form,
14+
FormControl,
15+
FormField,
16+
FormItem,
17+
FormLabel,
18+
FormMessage,
19+
} from "../../../../../../../../../@/components/ui/form";
20+
import { FormDescription } from "../../../../../../../../../@/components/ui/form";
21+
import { Input } from "../../../../../../../../../@/components/ui/input";
922
import { type Ecosystem, authOptions } from "../../../../types";
1023
import { useUpdateEcosystem } from "../../hooks/use-update-ecosystem";
1124

@@ -25,65 +38,205 @@ export function AuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
2538
} = useUpdateEcosystem({
2639
onError: (error) => {
2740
const message =
28-
error instanceof Error ? error.message : "Failed to create ecosystem";
41+
error instanceof Error ? error.message : "Failed to update ecosystem";
2942
toast.error(message);
3043
},
3144
});
3245

3346
return (
34-
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-5 md:gap-2">
35-
{authOptions.map((option) => (
36-
<CheckboxWithLabel
37-
key={option}
38-
className={cn(
39-
isPending &&
40-
variables?.authOptions?.includes(option) &&
41-
"animate-pulse",
42-
"hover:cursor-pointer hover:text-foreground",
43-
)}
44-
>
45-
<Checkbox
46-
checked={ecosystem.authOptions.includes(option)}
47-
onClick={() => {
48-
if (ecosystem.authOptions.includes(option)) {
49-
setMessageToConfirm({
50-
title: `Are you sure you want to remove ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
51-
description:
52-
"Users will no longer be able to log into your ecosystem using this option. Any users that previously used this option will be unable to log in.",
53-
authOptions: ecosystem.authOptions.filter(
54-
(o) => o !== option,
55-
),
56-
});
57-
} else {
58-
setMessageToConfirm({
59-
title: `Are you sure you want to add ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
60-
description:
61-
"Users will be able to log into your ecosystem using this option. If you later remove this option users that used it will no longer be able to log in.",
62-
authOptions: [...ecosystem.authOptions, option],
63-
});
64-
}
65-
}}
66-
/>
67-
{option.slice(0, 1).toUpperCase() + option.slice(1)}
68-
</CheckboxWithLabel>
69-
))}
70-
<ConfirmationDialog
71-
open={!!messageToConfirm}
72-
onOpenChange={(open) => {
73-
if (!open) {
74-
setMessageToConfirm(undefined);
75-
}
76-
}}
77-
title={messageToConfirm?.title}
78-
description={messageToConfirm?.description}
79-
onSubmit={() => {
80-
invariant(messageToConfirm, "Must have message for modal to be open");
81-
updateEcosystem({
82-
ecosystem,
83-
authOptions: messageToConfirm.authOptions,
84-
});
85-
}}
86-
/>
47+
<div className="flex flex-col gap-8">
48+
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-5 md:gap-2">
49+
{authOptions.map((option) => (
50+
<CheckboxWithLabel
51+
key={option}
52+
className={cn(
53+
isPending &&
54+
variables?.authOptions?.includes(option) &&
55+
"animate-pulse",
56+
"hover:cursor-pointer hover:text-foreground",
57+
)}
58+
>
59+
<Checkbox
60+
checked={ecosystem.authOptions?.includes(option)}
61+
onClick={() => {
62+
if (ecosystem.authOptions?.includes(option)) {
63+
setMessageToConfirm({
64+
title: `Are you sure you want to remove ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
65+
description:
66+
"Users will no longer be able to log into your ecosystem using this option. Any users that previously used this option will be unable to log in.",
67+
authOptions: ecosystem.authOptions?.filter(
68+
(o) => o !== option,
69+
),
70+
});
71+
} else {
72+
setMessageToConfirm({
73+
title: `Are you sure you want to add ${option.slice(0, 1).toUpperCase() + option.slice(1)} as an authentication option for this ecosystem?`,
74+
description:
75+
"Users will be able to log into your ecosystem using this option. If you later remove this option users that used it will no longer be able to log in.",
76+
authOptions: [...ecosystem.authOptions, option],
77+
});
78+
}
79+
}}
80+
/>
81+
{option.slice(0, 1).toUpperCase() + option.slice(1)}
82+
</CheckboxWithLabel>
83+
))}
84+
<ConfirmationDialog
85+
open={!!messageToConfirm}
86+
onOpenChange={(open) => {
87+
if (!open) {
88+
setMessageToConfirm(undefined);
89+
}
90+
}}
91+
title={messageToConfirm?.title}
92+
description={messageToConfirm?.description}
93+
onSubmit={() => {
94+
invariant(
95+
messageToConfirm,
96+
"Must have message for modal to be open",
97+
);
98+
updateEcosystem({
99+
...ecosystem,
100+
authOptions: messageToConfirm.authOptions,
101+
});
102+
}}
103+
/>
104+
</div>
105+
<CustomAuthOptionsForm ecosystem={ecosystem} />
106+
</div>
107+
);
108+
}
109+
110+
export function CustomAuthOptionsForm({ ecosystem }: { ecosystem: Ecosystem }) {
111+
const form = useForm({
112+
defaultValues: {
113+
customAuthEndpoint: ecosystem.customAuthOptions?.authEndpoint?.url,
114+
customHeaders: ecosystem.customAuthOptions?.authEndpoint?.headers,
115+
},
116+
});
117+
const { fields, remove, append } = useFieldArray({
118+
control: form.control,
119+
name: "customHeaders",
120+
});
121+
const { mutateAsync: updateEcosystem, isPending } = useUpdateEcosystem({
122+
onError: (error) => {
123+
const message =
124+
error instanceof Error ? error.message : "Failed to update ecosystem";
125+
toast.error(message);
126+
},
127+
onSuccess: () => {
128+
toast.success("Custom Auth Options updated");
129+
},
130+
});
131+
return (
132+
<div className="flex flex-col gap-4">
133+
<h4 className="font-semibold text-2xl text-foreground">
134+
Custom Auth Options
135+
</h4>
136+
<Card className="flex flex-col gap-4 p-4">
137+
<div className="flex flex-col gap-4">
138+
<Form {...form}>
139+
<FormField
140+
control={form.control}
141+
name="customAuthEndpoint"
142+
render={({ field }) => (
143+
<FormItem>
144+
<FormLabel>Custom Authentication Endpoint</FormLabel>
145+
<FormDescription>
146+
Enter the URL for your custom authentication endpoint
147+
</FormDescription>
148+
<FormControl>
149+
<Input
150+
{...field}
151+
type="url"
152+
placeholder="https://your-custom-auth-endpoint.com"
153+
/>
154+
</FormControl>
155+
<FormMessage />
156+
</FormItem>
157+
)}
158+
/>
159+
<FormField
160+
control={form.control}
161+
name="customHeaders"
162+
render={({ field }) => (
163+
<FormItem>
164+
<FormLabel>Custom Headers</FormLabel>
165+
<FormDescription>
166+
Optional: Add custom headers for your authentication
167+
endpoint
168+
</FormDescription>
169+
<FormControl>
170+
<div className="space-y-2">
171+
{fields.map((item, index) => (
172+
<div key={item.id} className="flex gap-2">
173+
<Input
174+
placeholder="Header Key"
175+
{...form.register(`customHeaders.${index}.key`)}
176+
/>
177+
<Input
178+
placeholder="Header Value"
179+
{...form.register(`customHeaders.${index}.value`)}
180+
/>
181+
<Button
182+
type="button"
183+
variant="destructive"
184+
onClick={() => remove(index)}
185+
>
186+
Remove
187+
</Button>
188+
</div>
189+
))}
190+
<Button
191+
type="button"
192+
variant="secondary"
193+
onClick={() => append({ key: "", value: "" })}
194+
>
195+
Add Header
196+
</Button>
197+
</div>
198+
</FormControl>
199+
<FormMessage />
200+
</FormItem>
201+
)}
202+
/>
203+
204+
<div className="flex justify-end">
205+
<Button
206+
disabled={isPending}
207+
type="submit"
208+
onClick={() => {
209+
const customAuthEndpoint =
210+
form.getValues("customAuthEndpoint");
211+
if (!customAuthEndpoint) {
212+
toast.error("Custom Auth Endpoint is required");
213+
return;
214+
}
215+
try {
216+
const url = new URL(customAuthEndpoint);
217+
invariant(url.hostname, "Invalid URL");
218+
} catch {
219+
toast.error("Invalid URL");
220+
return;
221+
}
222+
const customHeaders = form.getValues("customHeaders");
223+
updateEcosystem({
224+
...ecosystem,
225+
customAuthOptions: {
226+
authEndpoint: {
227+
url: customAuthEndpoint,
228+
headers: customHeaders,
229+
},
230+
},
231+
});
232+
}}
233+
>
234+
{isPending ? "Saving..." : "Save"}
235+
</Button>
236+
</div>
237+
</Form>
238+
</div>
239+
</Card>
87240
</div>
88241
);
89242
}

apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/components/client/integration-permissions-toggle.client.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ export function IntegrationPermissionsToggle({
8989
onSubmit={() => {
9090
invariant(messageToConfirm, "Must have message for modal to be open");
9191
updateEcosystem({
92-
ecosystem,
92+
...ecosystem,
9393
permission: messageToConfirm.permission,
9494
});
9595
}}

apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/[slug]/(active)/hooks/use-update-ecosystem.ts

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,30 @@ import {
44
useMutation,
55
useQueryClient,
66
} from "@tanstack/react-query";
7-
import type { AuthOption, Ecosystem } from "../../../types";
8-
9-
type UpdateEcosystemParams = {
10-
ecosystem: Ecosystem;
11-
permission?: "PARTNER_WHITELIST" | "ANYONE";
12-
authOptions?: AuthOption[];
13-
};
7+
import type { Ecosystem } from "../../../types";
148

159
export function useUpdateEcosystem(
16-
options?: Omit<
17-
UseMutationOptions<boolean, unknown, UpdateEcosystemParams>,
18-
"mutationFn"
19-
>,
10+
options?: Omit<UseMutationOptions<boolean, unknown, Ecosystem>, "mutationFn">,
2011
) {
2112
const { onSuccess, ...queryOptions } = options || {};
2213
const { isLoggedIn, user } = useLoggedInUser();
2314
const queryClient = useQueryClient();
2415

2516
return useMutation({
2617
// Returns true if the update was successful
27-
mutationFn: async (params: UpdateEcosystemParams): Promise<boolean> => {
18+
mutationFn: async (params: Ecosystem): Promise<boolean> => {
2819
if (!isLoggedIn || !user?.jwt) {
2920
throw new Error("Please login to update this ecosystem");
3021
}
3122

32-
const res = await fetch(
33-
`${params.ecosystem.url}/${params.ecosystem.id}`,
34-
{
35-
method: "PATCH",
36-
headers: {
37-
"Content-Type": "application/json",
38-
Authorization: `Bearer ${user.jwt}`,
39-
},
40-
body: JSON.stringify({
41-
permission: params.permission,
42-
authOptions: params.authOptions,
43-
}),
23+
const res = await fetch(`${params.url}/${params.id}`, {
24+
method: "PATCH",
25+
headers: {
26+
"Content-Type": "application/json",
27+
Authorization: `Bearer ${user.jwt}`,
4428
},
45-
);
29+
body: JSON.stringify(params),
30+
});
4631

4732
if (!res.ok) {
4833
const body = await res.json();

apps/dashboard/src/app/(dashboard)/dashboard/connect/ecosystem/types.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,21 @@ export type Ecosystem = {
2222
slug: string;
2323
permission: "PARTNER_WHITELIST" | "ANYONE";
2424
authOptions: (typeof authOptions)[number][];
25+
customAuthOptions?: {
26+
authEndpoint?: {
27+
url: string;
28+
headers?: { key: string; value: string }[];
29+
};
30+
jwt?: {
31+
jwksUri: string;
32+
aud: string;
33+
};
34+
};
35+
smartAccountOptions?: {
36+
chainIds: number[];
37+
sponsorGas: boolean;
38+
accountFactoryAddress: string;
39+
};
2540
url: string;
2641
status: "active" | "requested" | "paymentFailed";
2742
createdAt: string;

0 commit comments

Comments
 (0)