mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
added editing oauth clients
This commit is contained in:
@@ -238,13 +238,14 @@ async def execute_agent(
|
||||
detail="Webhook URL not in allowed domains for this client",
|
||||
)
|
||||
|
||||
# Store webhook registration
|
||||
# Store webhook registration with client's webhook secret
|
||||
await prisma.executionwebhook.create(
|
||||
data={ # type: ignore[typeddict-item]
|
||||
"executionId": graph_exec.id,
|
||||
"webhookUrl": request.webhook_url,
|
||||
"clientId": client.id,
|
||||
"userId": token.user_id,
|
||||
"secret": client.webhookSecret,
|
||||
}
|
||||
)
|
||||
logger.info(
|
||||
|
||||
@@ -16,6 +16,7 @@ import secrets
|
||||
from autogpt_libs.auth import get_user_id
|
||||
from fastapi import APIRouter, HTTPException, Security
|
||||
from prisma.enums import OAuthClientStatus
|
||||
from pydantic import BaseModel
|
||||
|
||||
from backend.data.db import prisma
|
||||
from backend.server.oauth.models import (
|
||||
@@ -39,6 +40,11 @@ def _generate_client_secret() -> str:
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def _generate_webhook_secret() -> str:
|
||||
"""Generate a secure webhook secret for HMAC signing."""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def _hash_secret(secret: str, salt: str) -> str:
|
||||
"""Hash a client secret with salt."""
|
||||
return hashlib.sha256(f"{salt}{secret}".encode()).hexdigest()
|
||||
@@ -59,7 +65,7 @@ def _client_to_response(client) -> ClientResponse:
|
||||
redirect_uris=client.redirectUris,
|
||||
allowed_scopes=client.allowedScopes,
|
||||
webhook_domains=client.webhookDomains,
|
||||
status=client.status.value,
|
||||
status=client.status,
|
||||
created_at=client.createdAt,
|
||||
updated_at=client.updatedAt,
|
||||
)
|
||||
@@ -87,6 +93,7 @@ async def register_client(
|
||||
|
||||
The client is immediately active (no admin approval required).
|
||||
For confidential clients, the client_secret is returned only once.
|
||||
The webhook_secret is always generated and returned only once.
|
||||
"""
|
||||
# Generate client credentials
|
||||
client_id = _generate_client_id()
|
||||
@@ -99,6 +106,9 @@ async def register_client(
|
||||
client_secret_salt = secrets.token_urlsafe(16)
|
||||
client_secret_hash = _hash_secret(client_secret, client_secret_salt)
|
||||
|
||||
# Generate webhook secret for HMAC signing
|
||||
webhook_secret = _generate_webhook_secret()
|
||||
|
||||
# Create client
|
||||
await prisma.oauthclient.create(
|
||||
data={ # type: ignore[typeddict-item]
|
||||
@@ -121,6 +131,7 @@ async def register_client(
|
||||
"redirectUris": request.redirect_uris,
|
||||
"allowedScopes": DEFAULT_ALLOWED_SCOPES,
|
||||
"webhookDomains": request.webhook_domains,
|
||||
"webhookSecret": webhook_secret,
|
||||
"status": OAuthClientStatus.ACTIVE,
|
||||
"ownerId": user_id,
|
||||
}
|
||||
@@ -129,6 +140,7 @@ async def register_client(
|
||||
return ClientSecretResponse(
|
||||
client_id=client_id,
|
||||
client_secret=client_secret or "",
|
||||
webhook_secret=webhook_secret,
|
||||
)
|
||||
|
||||
|
||||
@@ -236,6 +248,7 @@ async def rotate_client_secret(
|
||||
Rotate the client secret for a confidential client.
|
||||
|
||||
The new secret is returned only once. All existing tokens remain valid.
|
||||
Also rotates the webhook secret for security.
|
||||
"""
|
||||
client = await prisma.oauthclient.find_first(
|
||||
where={"clientId": client_id, "ownerId": user_id}
|
||||
@@ -250,22 +263,65 @@ async def rotate_client_secret(
|
||||
detail="Cannot rotate secret for public clients",
|
||||
)
|
||||
|
||||
# Generate new secret
|
||||
# Generate new secrets
|
||||
new_secret = _generate_client_secret()
|
||||
new_salt = secrets.token_urlsafe(16)
|
||||
new_hash = _hash_secret(new_secret, new_salt)
|
||||
new_webhook_secret = _generate_webhook_secret()
|
||||
|
||||
await prisma.oauthclient.update(
|
||||
where={"id": client.id},
|
||||
data={
|
||||
"clientSecretHash": new_hash,
|
||||
"clientSecretSalt": new_salt,
|
||||
"webhookSecret": new_webhook_secret,
|
||||
},
|
||||
)
|
||||
|
||||
return ClientSecretResponse(
|
||||
client_id=client_id,
|
||||
client_secret=new_secret,
|
||||
webhook_secret=new_webhook_secret,
|
||||
)
|
||||
|
||||
|
||||
class WebhookSecretResponse(BaseModel):
|
||||
"""Response containing newly generated webhook secret."""
|
||||
|
||||
client_id: str
|
||||
webhook_secret: str
|
||||
|
||||
|
||||
@client_router.post(
|
||||
"/{client_id}/rotate-webhook-secret", response_model=WebhookSecretResponse
|
||||
)
|
||||
async def rotate_webhook_secret(
|
||||
client_id: str,
|
||||
user_id: str = Security(get_user_id),
|
||||
) -> WebhookSecretResponse:
|
||||
"""
|
||||
Rotate only the webhook secret for a client.
|
||||
|
||||
The new webhook secret is returned only once.
|
||||
"""
|
||||
client = await prisma.oauthclient.find_first(
|
||||
where={"clientId": client_id, "ownerId": user_id}
|
||||
)
|
||||
|
||||
if not client:
|
||||
raise HTTPException(status_code=404, detail="Client not found")
|
||||
|
||||
# Generate new webhook secret
|
||||
new_webhook_secret = _generate_webhook_secret()
|
||||
|
||||
await prisma.oauthclient.update(
|
||||
where={"id": client.id},
|
||||
data={"webhookSecret": new_webhook_secret},
|
||||
)
|
||||
|
||||
return WebhookSecretResponse(
|
||||
client_id=client_id,
|
||||
webhook_secret=new_webhook_secret,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -236,12 +236,16 @@ class ClientResponse(BaseModel):
|
||||
|
||||
|
||||
class ClientSecretResponse(BaseModel):
|
||||
"""Response containing newly generated client secret."""
|
||||
"""Response containing newly generated client credentials."""
|
||||
|
||||
client_id: str
|
||||
client_secret: str = Field(
|
||||
..., description="Client secret (only shown once, store securely)"
|
||||
)
|
||||
webhook_secret: str = Field(
|
||||
...,
|
||||
description="Webhook secret for HMAC signing (only shown once, store securely)",
|
||||
)
|
||||
|
||||
|
||||
# ============================================================
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "platform"."OAuthClient" ADD COLUMN "webhookSecret" TEXT;
|
||||
@@ -1008,6 +1008,7 @@ model OAuthClient {
|
||||
redirectUris String[]
|
||||
allowedScopes String[]
|
||||
webhookDomains String[] // For webhook URL validation
|
||||
webhookSecret String? // Secret for HMAC signing webhooks
|
||||
|
||||
// Security
|
||||
requirePkce Boolean @default(true)
|
||||
|
||||
@@ -36,6 +36,7 @@ export function OAuthClientModals() {
|
||||
handleCreateClient,
|
||||
handleCopyClientId,
|
||||
handleCopyClientSecret,
|
||||
handleCopyWebhookSecret,
|
||||
resetForm,
|
||||
} = useOAuthClientModals();
|
||||
|
||||
@@ -211,12 +212,12 @@ export function OAuthClientModals() {
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={isSecretDialogOpen} onOpenChange={setIsSecretDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogContent className="sm:max-w-[525px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>OAuth Client Created</DialogTitle>
|
||||
<DialogDescription>
|
||||
Please copy your client credentials now. The client secret will
|
||||
not be shown again!
|
||||
Please copy your client credentials now. These secrets will not be
|
||||
shown again!
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
@@ -226,7 +227,11 @@ export function OAuthClientModals() {
|
||||
<code className="flex-1 rounded-md bg-secondary p-2 font-mono text-sm">
|
||||
{newClientSecret?.client_id}
|
||||
</code>
|
||||
<Button size="icon" variant="outline" onClick={handleCopyClientId}>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={handleCopyClientId}
|
||||
>
|
||||
<LuCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -246,11 +251,31 @@ export function OAuthClientModals() {
|
||||
<LuCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-destructive">
|
||||
This secret will only be shown once. Store it securely!
|
||||
</div>
|
||||
)}
|
||||
{newClientSecret?.webhook_secret && (
|
||||
<div className="space-y-2">
|
||||
<Label>Webhook Secret</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<code className="flex-1 break-all rounded-md bg-secondary p-2 font-mono text-sm">
|
||||
{newClientSecret.webhook_secret}
|
||||
</code>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={handleCopyWebhookSecret}
|
||||
>
|
||||
<LuCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Use this secret to verify webhook signatures (HMAC-SHA256)
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-destructive">
|
||||
These secrets will only be shown once. Store them securely!
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setIsSecretDialogOpen(false)}>Close</Button>
|
||||
|
||||
@@ -12,6 +12,11 @@ import { useState } from "react";
|
||||
|
||||
type ClientType = "public" | "confidential";
|
||||
|
||||
// Extended type to include webhook_secret (will be in generated types after API regeneration)
|
||||
interface ClientSecretResponseWithWebhook extends ClientSecretResponse {
|
||||
webhook_secret?: string;
|
||||
}
|
||||
|
||||
interface ClientFormState {
|
||||
name: string;
|
||||
description: string;
|
||||
@@ -36,7 +41,8 @@ export function useOAuthClientModals() {
|
||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||
const [isSecretDialogOpen, setIsSecretDialogOpen] = useState(false);
|
||||
const [formState, setFormState] = useState<ClientFormState>(initialFormState);
|
||||
const [newClientSecret, setNewClientSecret] = useState<ClientSecretResponse | null>(null);
|
||||
const [newClientSecret, setNewClientSecret] =
|
||||
useState<ClientSecretResponseWithWebhook | null>(null);
|
||||
|
||||
const queryClient = getQueryClient();
|
||||
const { toast } = useToast();
|
||||
@@ -117,7 +123,7 @@ export function useOAuthClientModals() {
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
const secretData = response.data as ClientSecretResponse;
|
||||
const secretData = response.data as ClientSecretResponseWithWebhook;
|
||||
setNewClientSecret(secretData);
|
||||
setIsCreateOpen(false);
|
||||
setIsSecretDialogOpen(true);
|
||||
@@ -140,7 +146,7 @@ export function useOAuthClientModals() {
|
||||
try {
|
||||
const response = await rotateSecret({ clientId });
|
||||
if (response.status === 200) {
|
||||
const secretData = response.data as ClientSecretResponse;
|
||||
const secretData = response.data as ClientSecretResponseWithWebhook;
|
||||
setNewClientSecret(secretData);
|
||||
setIsSecretDialogOpen(true);
|
||||
toast({
|
||||
@@ -177,6 +183,16 @@ export function useOAuthClientModals() {
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopyWebhookSecret() {
|
||||
if (newClientSecret?.webhook_secret) {
|
||||
navigator.clipboard.writeText(newClientSecret.webhook_secret);
|
||||
toast({
|
||||
title: "Copied",
|
||||
description: "Webhook secret copied to clipboard",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleSecretDialogChange(open: boolean) {
|
||||
setIsSecretDialogOpen(open);
|
||||
if (!open) {
|
||||
@@ -198,6 +214,7 @@ export function useOAuthClientModals() {
|
||||
handleRotateSecret,
|
||||
handleCopyClientId,
|
||||
handleCopyClientSecret,
|
||||
handleCopyWebhookSecret,
|
||||
resetForm,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Loader2, MoreVertical } from "lucide-react";
|
||||
import { LuCopy } from "react-icons/lu";
|
||||
import { Button } from "@/components/__legacy__/ui/button";
|
||||
import {
|
||||
Table,
|
||||
@@ -18,6 +19,17 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/__legacy__/ui/dropdown-menu";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/__legacy__/ui/dialog";
|
||||
import { Label } from "@/components/__legacy__/ui/label";
|
||||
import { Input } from "@/components/__legacy__/ui/input";
|
||||
import { Textarea } from "@/components/__legacy__/ui/textarea";
|
||||
import { useOAuthClientSection } from "./useOAuthClientSection";
|
||||
|
||||
export function OAuthClientSection() {
|
||||
@@ -27,12 +39,31 @@ export function OAuthClientSection() {
|
||||
isDeleting,
|
||||
isSuspending,
|
||||
isActivating,
|
||||
isRotatingWebhookSecret,
|
||||
isUpdating,
|
||||
handleDeleteClient,
|
||||
handleSuspendClient,
|
||||
handleActivateClient,
|
||||
handleRotateWebhookSecret,
|
||||
handleCopyWebhookSecret,
|
||||
handleEditClient,
|
||||
handleSaveClient,
|
||||
webhookSecretDialogOpen,
|
||||
setWebhookSecretDialogOpen,
|
||||
newWebhookSecret,
|
||||
editDialogOpen,
|
||||
setEditDialogOpen,
|
||||
editingClient,
|
||||
editFormState,
|
||||
setEditFormState,
|
||||
} = useOAuthClientSection();
|
||||
|
||||
const isActionPending = isDeleting || isSuspending || isActivating;
|
||||
const isActionPending =
|
||||
isDeleting ||
|
||||
isSuspending ||
|
||||
isActivating ||
|
||||
isRotatingWebhookSecret ||
|
||||
isUpdating;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -106,6 +137,21 @@ export function OAuthClientSection() {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleEditClient(client)}
|
||||
disabled={isActionPending}
|
||||
>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() =>
|
||||
handleRotateWebhookSecret(client.client_id)
|
||||
}
|
||||
disabled={isActionPending}
|
||||
>
|
||||
Rotate Webhook Secret
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
{client.status === "ACTIVE" ? (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSuspendClient(client.client_id)}
|
||||
@@ -141,6 +187,196 @@ export function OAuthClientSection() {
|
||||
No OAuth clients registered yet. Create one to get started.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
open={webhookSecretDialogOpen}
|
||||
onOpenChange={setWebhookSecretDialogOpen}
|
||||
>
|
||||
<DialogContent className="sm:max-w-[525px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Webhook Secret Rotated</DialogTitle>
|
||||
<DialogDescription>
|
||||
Your new webhook secret has been generated. Please copy it now as
|
||||
it will not be shown again.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>New Webhook Secret</Label>
|
||||
<div className="flex items-center space-x-2">
|
||||
<code className="flex-1 break-all rounded-md bg-secondary p-2 font-mono text-sm">
|
||||
{newWebhookSecret}
|
||||
</code>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={handleCopyWebhookSecret}
|
||||
>
|
||||
<LuCopy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Use this secret to verify webhook signatures (HMAC-SHA256)
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-destructive">
|
||||
This secret will only be shown once. Store it securely!
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button onClick={() => setWebhookSecretDialogOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={editDialogOpen} onOpenChange={setEditDialogOpen}>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-[525px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit OAuth Client</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update your OAuth client settings. Changes will take effect
|
||||
immediately.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-name">Name</Label>
|
||||
<Input
|
||||
id="edit-name"
|
||||
value={editFormState.name ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditFormState((prev) => ({
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="My Application"
|
||||
maxLength={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-description">Description</Label>
|
||||
<Input
|
||||
id="edit-description"
|
||||
value={editFormState.description ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditFormState((prev) => ({
|
||||
...prev,
|
||||
description: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="A brief description of your application"
|
||||
maxLength={500}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-redirectUris">Redirect URIs</Label>
|
||||
<Textarea
|
||||
id="edit-redirectUris"
|
||||
value={editFormState.redirect_uris?.join("\n") ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditFormState((prev) => ({
|
||||
...prev,
|
||||
redirect_uris: e.target.value
|
||||
.split(/[\n,]/)
|
||||
.map((uri) => uri.trim())
|
||||
.filter(Boolean),
|
||||
}))
|
||||
}
|
||||
placeholder="https://myapp.com/callback https://localhost:3000/callback"
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Enter one URI per line or separate with commas
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-webhookDomains">Webhook Domains</Label>
|
||||
<Textarea
|
||||
id="edit-webhookDomains"
|
||||
value={editFormState.webhook_domains?.join("\n") ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditFormState((prev) => ({
|
||||
...prev,
|
||||
webhook_domains: e.target.value
|
||||
.split(/[\n,]/)
|
||||
.map((domain) => domain.trim())
|
||||
.filter(Boolean),
|
||||
}))
|
||||
}
|
||||
placeholder="https://myapp.com https://api.myapp.com"
|
||||
rows={3}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Domains that can receive webhook notifications
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-homepageUrl">Homepage URL</Label>
|
||||
<Input
|
||||
id="edit-homepageUrl"
|
||||
type="url"
|
||||
value={editFormState.homepage_url ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditFormState((prev) => ({
|
||||
...prev,
|
||||
homepage_url: e.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
placeholder="https://myapp.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-privacyPolicyUrl">Privacy Policy URL</Label>
|
||||
<Input
|
||||
id="edit-privacyPolicyUrl"
|
||||
type="url"
|
||||
value={editFormState.privacy_policy_url ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditFormState((prev) => ({
|
||||
...prev,
|
||||
privacy_policy_url: e.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
placeholder="https://myapp.com/privacy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-termsOfServiceUrl">
|
||||
Terms of Service URL
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-termsOfServiceUrl"
|
||||
type="url"
|
||||
value={editFormState.terms_of_service_url ?? ""}
|
||||
onChange={(e) =>
|
||||
setEditFormState((prev) => ({
|
||||
...prev,
|
||||
terms_of_service_url: e.target.value || undefined,
|
||||
}))
|
||||
}
|
||||
placeholder="https://myapp.com/terms"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setEditDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveClient} disabled={isUpdating}>
|
||||
{isUpdating ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
getGetOauthClientsListClientsQueryKey,
|
||||
useDeleteOauthClientsDeleteClient,
|
||||
useGetOauthClientsListClients,
|
||||
usePatchOauthClientsUpdateClient,
|
||||
usePostOauthClientsActivateClient,
|
||||
usePostOauthClientsRotateWebhookSecret,
|
||||
usePostOauthClientsSuspendClient,
|
||||
} from "@/app/api/__generated__/endpoints/oauth-clients/oauth-clients";
|
||||
import type { ClientResponse } from "@/app/api/__generated__/models/clientResponse";
|
||||
import type { UpdateClientRequest } from "@/app/api/__generated__/models/updateClientRequest";
|
||||
import { useToast } from "@/components/molecules/Toast/use-toast";
|
||||
import { getQueryClient } from "@/lib/react-query/queryClient";
|
||||
|
||||
@@ -13,6 +18,14 @@ export function useOAuthClientSection() {
|
||||
const queryClient = getQueryClient();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [webhookSecretDialogOpen, setWebhookSecretDialogOpen] = useState(false);
|
||||
const [newWebhookSecret, setNewWebhookSecret] = useState<string | null>(null);
|
||||
const [editDialogOpen, setEditDialogOpen] = useState(false);
|
||||
const [editingClient, setEditingClient] = useState<ClientResponse | null>(
|
||||
null,
|
||||
);
|
||||
const [editFormState, setEditFormState] = useState<UpdateClientRequest>({});
|
||||
|
||||
const { data: oauthClients, isLoading } = useGetOauthClientsListClients({
|
||||
query: {
|
||||
select: (res) => {
|
||||
@@ -55,6 +68,22 @@ export function useOAuthClientSection() {
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
mutateAsync: rotateWebhookSecret,
|
||||
isPending: isRotatingWebhookSecret,
|
||||
} = usePostOauthClientsRotateWebhookSecret();
|
||||
|
||||
const { mutateAsync: updateClient, isPending: isUpdating } =
|
||||
usePatchOauthClientsUpdateClient({
|
||||
mutation: {
|
||||
onSettled: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: getGetOauthClientsListClientsQueryKey(),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
async function handleDeleteClient(clientId: string) {
|
||||
try {
|
||||
await deleteClient({ clientId });
|
||||
@@ -103,14 +132,95 @@ export function useOAuthClientSection() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRotateWebhookSecret(clientId: string) {
|
||||
try {
|
||||
const response = await rotateWebhookSecret({ clientId });
|
||||
if (response.status === 200) {
|
||||
setNewWebhookSecret(response.data.webhook_secret);
|
||||
setWebhookSecretDialogOpen(true);
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Webhook secret rotated successfully",
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to rotate webhook secret",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleCopyWebhookSecret() {
|
||||
if (newWebhookSecret) {
|
||||
navigator.clipboard.writeText(newWebhookSecret);
|
||||
toast({
|
||||
title: "Copied",
|
||||
description: "Webhook secret copied to clipboard",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditClient(client: ClientResponse) {
|
||||
setEditingClient(client);
|
||||
setEditFormState({
|
||||
name: client.name,
|
||||
description: client.description ?? undefined,
|
||||
homepage_url: client.homepage_url ?? undefined,
|
||||
privacy_policy_url: client.privacy_policy_url ?? undefined,
|
||||
terms_of_service_url: client.terms_of_service_url ?? undefined,
|
||||
redirect_uris: client.redirect_uris,
|
||||
webhook_domains: client.webhook_domains,
|
||||
});
|
||||
setEditDialogOpen(true);
|
||||
}
|
||||
|
||||
async function handleSaveClient() {
|
||||
if (!editingClient) return;
|
||||
|
||||
try {
|
||||
await updateClient({
|
||||
clientId: editingClient.client_id,
|
||||
data: editFormState,
|
||||
});
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "OAuth client updated successfully",
|
||||
});
|
||||
setEditDialogOpen(false);
|
||||
setEditingClient(null);
|
||||
} catch {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to update OAuth client",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
oauthClients,
|
||||
isLoading,
|
||||
isDeleting,
|
||||
isSuspending,
|
||||
isActivating,
|
||||
isRotatingWebhookSecret,
|
||||
isUpdating,
|
||||
handleDeleteClient,
|
||||
handleSuspendClient,
|
||||
handleActivateClient,
|
||||
handleRotateWebhookSecret,
|
||||
handleCopyWebhookSecret,
|
||||
handleEditClient,
|
||||
handleSaveClient,
|
||||
webhookSecretDialogOpen,
|
||||
setWebhookSecretDialogOpen,
|
||||
newWebhookSecret,
|
||||
editDialogOpen,
|
||||
setEditDialogOpen,
|
||||
editingClient,
|
||||
editFormState,
|
||||
setEditFormState,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5663,7 +5663,7 @@
|
||||
"post": {
|
||||
"tags": ["oauth-clients", "oauth-clients"],
|
||||
"summary": "Register Client",
|
||||
"description": "Register a new OAuth client.\n\nThe client is immediately active (no admin approval required).\nFor confidential clients, the client_secret is returned only once.",
|
||||
"description": "Register a new OAuth client.\n\nThe client is immediately active (no admin approval required).\nFor confidential clients, the client_secret is returned only once.\nThe webhook_secret is always generated and returned only once.",
|
||||
"operationId": "postOauth-clientsRegisterClient",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -5825,7 +5825,7 @@
|
||||
"post": {
|
||||
"tags": ["oauth-clients", "oauth-clients"],
|
||||
"summary": "Rotate Client Secret",
|
||||
"description": "Rotate the client secret for a confidential client.\n\nThe new secret is returned only once. All existing tokens remain valid.",
|
||||
"description": "Rotate the client secret for a confidential client.\n\nThe new secret is returned only once. All existing tokens remain valid.\nAlso rotates the webhook secret for security.",
|
||||
"operationId": "postOauth-clientsRotateClientSecret",
|
||||
"security": [{ "HTTPBearerJWT": [] }],
|
||||
"parameters": [
|
||||
@@ -5861,6 +5861,46 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oauth/clients/{client_id}/rotate-webhook-secret": {
|
||||
"post": {
|
||||
"tags": ["oauth-clients", "oauth-clients"],
|
||||
"summary": "Rotate Webhook Secret",
|
||||
"description": "Rotate only the webhook secret for a client.\n\nThe new webhook secret is returned only once.",
|
||||
"operationId": "postOauth-clientsRotateWebhookSecret",
|
||||
"security": [{ "HTTPBearerJWT": [] }],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "client_id",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": { "type": "string", "title": "Client Id" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successful Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/WebhookSecretResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Validation Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"$ref": "#/components/responses/HTTP401NotAuthenticatedError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/oauth/clients/{client_id}/suspend": {
|
||||
"post": {
|
||||
"tags": ["oauth-clients", "oauth-clients"],
|
||||
@@ -6901,12 +6941,17 @@
|
||||
"type": "string",
|
||||
"title": "Client Secret",
|
||||
"description": "Client secret (only shown once, store securely)"
|
||||
},
|
||||
"webhook_secret": {
|
||||
"type": "string",
|
||||
"title": "Webhook Secret",
|
||||
"description": "Webhook secret for HMAC signing (only shown once, store securely)"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["client_id", "client_secret"],
|
||||
"required": ["client_id", "client_secret", "webhook_secret"],
|
||||
"title": "ClientSecretResponse",
|
||||
"description": "Response containing newly generated client secret."
|
||||
"description": "Response containing newly generated client credentials."
|
||||
},
|
||||
"CountResponse": {
|
||||
"properties": {
|
||||
@@ -12109,6 +12154,16 @@
|
||||
"url"
|
||||
],
|
||||
"title": "Webhook"
|
||||
},
|
||||
"WebhookSecretResponse": {
|
||||
"properties": {
|
||||
"client_id": { "type": "string", "title": "Client Id" },
|
||||
"webhook_secret": { "type": "string", "title": "Webhook Secret" }
|
||||
},
|
||||
"type": "object",
|
||||
"required": ["client_id", "webhook_secret"],
|
||||
"title": "WebhookSecretResponse",
|
||||
"description": "Response containing newly generated webhook secret."
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
|
||||
1013
docs/content/platform/integrations/nextjs.md
Normal file
1013
docs/content/platform/integrations/nextjs.md
Normal file
File diff suppressed because it is too large
Load Diff
1092
docs/content/platform/integrations/rails.md
Normal file
1092
docs/content/platform/integrations/rails.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,9 @@ nav:
|
||||
- Using D-ID: platform/d_id.md
|
||||
- Blocks: platform/blocks/blocks.md
|
||||
- External API Integration: platform/external-api-integration.md
|
||||
- Framework Integrations:
|
||||
- Next.js: platform/integrations/nextjs.md
|
||||
- Ruby on Rails: platform/integrations/rails.md
|
||||
- Contributing:
|
||||
- Tests: platform/contributing/tests.md
|
||||
- OAuth Flows: platform/contributing/oauth-integration-flow.md
|
||||
|
||||
Reference in New Issue
Block a user