Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions app/dashboard/organization/clients/actions.ts
Original file line number Diff line number Diff line change
@@ -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",
}
)
127 changes: 127 additions & 0 deletions app/dashboard/organization/clients/clients-list.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Card>
<CardHeader>
<CardTitle>API Clients</CardTitle>
<CardDescription>
The current API clients for your organization.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Client</TableHead>
<TableHead>Type</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{clients.map((client) => (
<TableRow key={client.id}>
<TableCell>
<div className="flex items-center space-x-4">
<Avatar>
<AvatarFallback>
{client.name.charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium leading-none">
{client.name}
</p>
<p className="text-sm text-muted-foreground">
{client.id}
</p>
</div>
</div>
</TableCell>
<TableCell>
<span className="capitalize">{client.type}</span>
</TableCell>
<TableCell className="flex justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="icon" variant="outline">
<DotsVerticalIcon className="size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onSelect={async () => {
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
}}
>
<ReloadIcon className="mr-1 size-4" />
Rotate Secret
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive"
onSelect={async () => {
const result = await deleteApiClient(client.id)
if (result?.error) {
return toast.error(result.error)
}
toast.success(`Deleted client: ${client.name}`)
}}
>
<TrashIcon className="mr-1 size-4" />
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)
}
106 changes: 106 additions & 0 deletions app/dashboard/organization/clients/create-client-form.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLFormElement>(null)

return (
<Card>
<form
ref={ref}
action={async (formData: FormData) => {
toast.promise(createApiClient(formData), {
loading: "Creating client...",
success: (result) => {
if ("error" in result) {
throw result.error
}

toast(() => <h5>Your client secret is:</h5>, {
description: () => (
<code>
{
(result as unknown as CreateApiClientSuccess)
?.clientSecret
}
</code>
),
duration: Infinity,
action: {
label: "Dismiss",
onClick: () => {},
},
})
return `API Client created: ${formData.get("name")}`
},
error: (err) => err,
})
}}
>
<CardHeader>
<CardTitle>Create API Client</CardTitle>
<CardDescription>
Create a new API client for your application to access this
organization&apos;s resources.
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex space-x-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="name">Client Name</Label>
<Input
type="text"
id="name"
name="name"
placeholder="My Application"
/>
</div>

<div className="grid w-full items-center gap-1.5">
<Label htmlFor="app_type">Application Type</Label>
<Select defaultValue="regular_web" name="app_type">
<SelectTrigger id="app_type">
<SelectValue placeholder="Select an application type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="native">Native</SelectItem>
<SelectItem value="spa">Single Page App</SelectItem>
<SelectItem value="regular_web">Regular Web App</SelectItem>
<SelectItem value="non_interactive">
Non Interactive
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
<CardFooter className="flex justify-end">
<SubmitButton>Create Client</SubmitButton>
</CardFooter>
</form>
</Card>
)
}
Loading