diff --git a/app/dashboard/organization/clients/actions.ts b/app/dashboard/organization/clients/actions.ts new file mode 100644 index 000000000..3aff7936e --- /dev/null +++ b/app/dashboard/organization/clients/actions.ts @@ -0,0 +1,144 @@ +"use server" + +import { revalidatePath } from "next/cache" +import { Session } from "@auth0/nextjs-auth0" + +import { managementClient } from "@/lib/auth0" +import { Client } from "@/lib/clients" +import { withServerActionAuth } from "@/lib/with-server-action-auth" + +interface CreateApiClientError { + error: string +} +export interface CreateApiClientSuccess { + clientId: string + clientSecret: string +} + +export const createApiClient = withServerActionAuth( + async function createApiClient(formData: FormData, _: Session) { + const name = formData.get("name") as Client["name"] + const app_type = formData.get("app_type") as Client["app_type"] + + if (!name) { + return { + error: "Client name is required.", + } as CreateApiClientError + } + + if (!app_type) { + return { + error: "Application type is required.", + } as CreateApiClientError + } + + try { + const { data: newClient } = await managementClient.clients.create({ + name, + app_type, + is_first_party: true, + }) + + revalidatePath("/dashboard/organization/api-clients") + return { + clientId: newClient.client_id, + clientSecret: newClient.client_secret, + } as CreateApiClientSuccess + } catch (error) { + console.error("Failed to create API client", error) + return { + error: "Failed to create API client.", + } as CreateApiClientError + } + }, + { + role: "admin", + } +) + +export const deleteApiClient = withServerActionAuth( + async function deleteApiClient(client_id: string, _: Session) { + try { + await managementClient.clients.delete({ client_id }) + + revalidatePath("/dashboard/organization/api-clients") + } catch (error) { + console.error("Failed to delete API client", error) + return { + error: "Failed to delete API client.", + } + } + + return {} + }, + { + role: "admin", + } +) + +export const updateApiClient = withServerActionAuth( + async function updateApiClient( + clientId: string, + formData: FormData, + _: Session + ) { + const name = formData.get("name") as Client["name"] + const app_type = formData.get("app_type") as Client["app_type"] + + if (!name) { + return { + error: "Client name is required.", + } + } + + if (!app_type) { + return { + error: "Application type is required.", + } + } + + try { + await managementClient.clients.update( + { client_id: clientId }, + { + name, + app_type, + } + ) + + revalidatePath("/dashboard/organization/api-clients") + } catch (error) { + console.error("Failed to update API client", error) + return { + error: "Failed to update API client.", + } + } + + return {} + }, + { + role: "admin", + } +) + +export const rotateApiClientSecret = withServerActionAuth( + async function rotateApiClientSecret(client_id: string, _: Session) { + try { + const { data: result } = + await managementClient.clients.rotateClientSecret({ + client_id, + }) + + revalidatePath("/dashboard/organization/api-clients") + return { clientSecret: result.client_secret } + } catch (error) { + console.error("Failed to rotate API client secret", error) + return { + error: "Failed to rotate API client secret.", + } + } + }, + { + role: "admin", + } +) diff --git a/app/dashboard/organization/clients/clients-list.tsx b/app/dashboard/organization/clients/clients-list.tsx new file mode 100644 index 000000000..945389208 --- /dev/null +++ b/app/dashboard/organization/clients/clients-list.tsx @@ -0,0 +1,127 @@ +"use client" + +import { DotsVerticalIcon, ReloadIcon, TrashIcon } from "@radix-ui/react-icons" +import { toast } from "sonner" + +import { Avatar, AvatarFallback } from "@/components/ui/avatar" +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" + +import { deleteApiClient, rotateApiClientSecret } from "./actions" + +interface Props { + clients: { + id: string + name: string + type: string + }[] +} + +export function ApiClientsList({ clients }: Props) { + return ( + + + API Clients + + The current API clients for your organization. + + + + + + + Client + Type + + + + + {clients.map((client) => ( + + +
+ + + {client.name.charAt(0).toUpperCase()} + + +
+

+ {client.name} +

+

+ {client.id} +

+
+
+
+ + {client.type} + + + + + + + + { + const result = await rotateApiClientSecret(client.id) + if (result.error) { + return toast.error(result.error) + } + toast.success( + `Rotated secret for client: ${client.name}` + ) + // You might want to display the new secret here, or copy it to clipboard + }} + > + + Rotate Secret + + { + const result = await deleteApiClient(client.id) + if (result?.error) { + return toast.error(result.error) + } + toast.success(`Deleted client: ${client.name}`) + }} + > + + Delete + + + + +
+ ))} +
+
+
+
+ ) +} diff --git a/app/dashboard/organization/clients/create-client-form.tsx b/app/dashboard/organization/clients/create-client-form.tsx new file mode 100644 index 000000000..72cce71f5 --- /dev/null +++ b/app/dashboard/organization/clients/create-client-form.tsx @@ -0,0 +1,106 @@ +"use client" + +import { useRef } from "react" +import { toast } from "sonner" + +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { SubmitButton } from "@/components/submit-button" + +import { createApiClient, CreateApiClientSuccess } from "./actions" + +export function CreateApiClientForm() { + const ref = useRef(null) + + return ( + +
{ + toast.promise(createApiClient(formData), { + loading: "Creating client...", + success: (result) => { + if ("error" in result) { + throw result.error + } + + toast(() =>
Your client secret is:
, { + description: () => ( + + { + (result as unknown as CreateApiClientSuccess) + ?.clientSecret + } + + ), + duration: Infinity, + action: { + label: "Dismiss", + onClick: () => {}, + }, + }) + return `API Client created: ${formData.get("name")}` + }, + error: (err) => err, + }) + }} + > + + Create API Client + + Create a new API client for your application to access this + organization's resources. + + + +
+
+ + +
+ +
+ + +
+
+
+ + Create Client + +
+
+ ) +} diff --git a/app/dashboard/organization/clients/page.tsx b/app/dashboard/organization/clients/page.tsx new file mode 100644 index 000000000..1bfa76fcd --- /dev/null +++ b/app/dashboard/organization/clients/page.tsx @@ -0,0 +1,31 @@ +import { appClient, managementClient } from "@/lib/auth0" +import { PageHeader } from "@/components/page-header" + +import { ApiClientsList } from "./clients-list" +import { CreateApiClientForm } from "./create-client-form" + +export default async function ApiClients() { + const session = await appClient.getSession() + const { data: apiClients } = await managementClient.clients.getAll({ + is_global: false, + }) + + return ( +
+ + + ({ + id: client.client_id, + name: client.name, + type: client.app_type, + }))} + /> + + +
+ ) +} diff --git a/app/dashboard/organization/layout.tsx b/app/dashboard/organization/layout.tsx index e19150591..7d321dfa1 100644 --- a/app/dashboard/organization/layout.tsx +++ b/app/dashboard/organization/layout.tsx @@ -23,6 +23,10 @@ const sidebarNavItems = [ title: "Members", href: "/dashboard/organization/members", }, + { + title: "API Clients", + href: "/dashboard/organization/clients", + }, { title: "SSO", href: "/dashboard/organization/sso", diff --git a/app/layout.tsx b/app/layout.tsx index 93f95819b..504a177b8 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -34,7 +34,7 @@ export default async function RootLayout({ {children} - +