Highly ambitious ATProtocol AppView service and sdks
at fix-postgres 287 lines 8.3 kB view raw
1import { useEffect, useState } from "react"; 2import { graphql, useMutation } from "react-relay"; 3import { Dialog } from "./Dialog.tsx"; 4import { Button } from "./Button.tsx"; 5import { Input } from "./Input.tsx"; 6import { Textarea } from "./Textarea.tsx"; 7import { FormControl } from "./FormControl.tsx"; 8import { CopyableField } from "./CopyableField.tsx"; 9import type { OAuthClientModalCreateMutation } from "../__generated__/OAuthClientModalCreateMutation.graphql.ts"; 10import type { OAuthClientModalUpdateMutation } from "../__generated__/OAuthClientModalUpdateMutation.graphql.ts"; 11 12interface OAuthClient { 13 clientId: string; 14 clientSecret?: string | null; 15 clientName: string; 16 redirectUris: ReadonlyArray<string>; 17 scope?: string | null; 18 clientUri?: string | null; 19 logoUri?: string | null; 20 tosUri?: string | null; 21 policyUri?: string | null; 22} 23 24interface OAuthClientModalProps { 25 sliceUri: string; 26 client?: OAuthClient | null; 27 onClose: () => void; 28 onSuccess: (clientId: string, clientSecret: string | null) => void; 29} 30 31export function OAuthClientModal({ 32 sliceUri, 33 client, 34 onClose, 35 onSuccess, 36}: OAuthClientModalProps) { 37 const isEditMode = !!client; 38 39 const [clientName, setClientName] = useState(""); 40 const [redirectUris, setRedirectUris] = useState(""); 41 const [scope, setScope] = useState(""); 42 const [clientUri, setClientUri] = useState(""); 43 const [logoUri, setLogoUri] = useState(""); 44 const [tosUri, setTosUri] = useState(""); 45 const [policyUri, setPolicyUri] = useState(""); 46 const [error, setError] = useState<string | null>(null); 47 48 // Initialize form with client data in edit mode 49 useEffect(() => { 50 if (client) { 51 setClientName(client.clientName); 52 setRedirectUris(client.redirectUris.join("\n")); 53 setScope(client.scope || ""); 54 setClientUri(client.clientUri || ""); 55 setLogoUri(client.logoUri || ""); 56 setTosUri(client.tosUri || ""); 57 setPolicyUri(client.policyUri || ""); 58 } 59 }, [client]); 60 61 const [createOAuthClient, isCreating] = useMutation< 62 OAuthClientModalCreateMutation 63 >(graphql` 64 mutation OAuthClientModalCreateMutation( 65 $sliceUri: String! 66 $clientName: String! 67 $redirectUris: [String!]! 68 $scope: String! 69 $clientUri: String 70 $logoUri: String 71 $tosUri: String 72 $policyUri: String 73 ) { 74 createOAuthClient( 75 sliceUri: $sliceUri 76 clientName: $clientName 77 redirectUris: $redirectUris 78 scope: $scope 79 clientUri: $clientUri 80 logoUri: $logoUri 81 tosUri: $tosUri 82 policyUri: $policyUri 83 ) { 84 clientId 85 clientSecret 86 } 87 } 88 `); 89 90 const [updateOAuthClient, isUpdating] = useMutation< 91 OAuthClientModalUpdateMutation 92 >(graphql` 93 mutation OAuthClientModalUpdateMutation( 94 $clientId: String! 95 $clientName: String 96 $redirectUris: [String!] 97 $scope: String 98 $clientUri: String 99 $logoUri: String 100 $tosUri: String 101 $policyUri: String 102 ) { 103 updateOAuthClient( 104 clientId: $clientId 105 clientName: $clientName 106 redirectUris: $redirectUris 107 scope: $scope 108 clientUri: $clientUri 109 logoUri: $logoUri 110 tosUri: $tosUri 111 policyUri: $policyUri 112 ) { 113 clientId 114 } 115 } 116 `); 117 118 const isSubmitting = isCreating || isUpdating; 119 120 const handleSubmit = (e: React.FormEvent) => { 121 e.preventDefault(); 122 setError(null); 123 124 // Parse redirect URIs 125 const uris = redirectUris 126 .split("\n") 127 .map((uri) => uri.trim()) 128 .filter((uri) => uri.length > 0); 129 130 if (uris.length === 0) { 131 setError("At least one redirect URI is required"); 132 return; 133 } 134 135 // Validate redirect URIs 136 for (const uri of uris) { 137 if (!uri.startsWith("http://") && !uri.startsWith("https://")) { 138 setError(`Invalid redirect URI: ${uri}. Must use HTTP or HTTPS.`); 139 return; 140 } 141 } 142 143 if (isEditMode && client) { 144 // Update existing client 145 updateOAuthClient({ 146 variables: { 147 clientId: client.clientId, 148 clientName: clientName.trim() || null, 149 redirectUris: uris, 150 scope: scope.trim() || null, 151 clientUri: clientUri.trim() || null, 152 logoUri: logoUri.trim() || null, 153 tosUri: tosUri.trim() || null, 154 policyUri: policyUri.trim() || null, 155 }, 156 onCompleted: (response) => { 157 onSuccess(response.updateOAuthClient.clientId, null); 158 }, 159 onError: (err) => { 160 console.error("Failed to update OAuth client:", err); 161 setError( 162 err.message || "Failed to update OAuth client. Please try again.", 163 ); 164 }, 165 }); 166 } else { 167 // Create new client 168 createOAuthClient({ 169 variables: { 170 sliceUri, 171 clientName, 172 redirectUris: uris, 173 scope: scope.trim(), 174 clientUri: clientUri.trim() || null, 175 logoUri: logoUri.trim() || null, 176 tosUri: tosUri.trim() || null, 177 policyUri: policyUri.trim() || null, 178 }, 179 onCompleted: (response) => { 180 onSuccess( 181 response.createOAuthClient.clientId, 182 response.createOAuthClient.clientSecret ?? null, 183 ); 184 }, 185 onError: (err) => { 186 console.error("Failed to create OAuth client:", err); 187 setError( 188 err.message || "Failed to create OAuth client. Please try again.", 189 ); 190 }, 191 }); 192 } 193 }; 194 195 return ( 196 <Dialog 197 open 198 onClose={onClose} 199 title={isEditMode ? "Edit OAuth Client" : "Register OAuth Client"} 200 maxWidth="2xl" 201 className="max-h-[90vh] overflow-y-auto" 202 > 203 {isEditMode && client && ( 204 <div className="mb-4 pb-4 border-b border-zinc-800 space-y-4"> 205 <CopyableField value={client.clientId} label="Client ID" /> 206 {client.clientSecret && ( 207 <CopyableField value={client.clientSecret} label="Client Secret" /> 208 )} 209 </div> 210 )} 211 212 <form onSubmit={handleSubmit} className="space-y-4"> 213 <FormControl label="Client Name" htmlFor="clientName"> 214 <Input 215 id="clientName" 216 value={clientName} 217 onChange={(e) => setClientName(e.target.value)} 218 placeholder="My Application" 219 required 220 disabled={isSubmitting} 221 /> 222 </FormControl> 223 224 <FormControl 225 label="Redirect URIs" 226 htmlFor="redirectUris" 227 > 228 <Textarea 229 id="redirectUris" 230 value={redirectUris} 231 onChange={(e) => setRedirectUris(e.target.value)} 232 placeholder="https://example.com/callback&#10;https://localhost:3000/callback" 233 required 234 disabled={isSubmitting} 235 rows={3} 236 className="bg-zinc-800 border-zinc-700 focus:border-blue-500 font-mono" 237 /> 238 </FormControl> 239 240 <FormControl 241 label="Scope" 242 htmlFor="scope" 243 > 244 <Input 245 id="scope" 246 value={scope} 247 onChange={(e) => setScope(e.target.value)} 248 placeholder="atproto" 249 required 250 disabled={isSubmitting} 251 /> 252 </FormControl> 253 254 {error && ( 255 <div className="bg-red-500/10 border border-red-500/20 text-red-400 px-3 py-2 rounded text-sm"> 256 {error} 257 </div> 258 )} 259 260 <div className="flex justify-end gap-3 pt-4"> 261 <Button 262 type="button" 263 onClick={onClose} 264 variant="default" 265 disabled={isSubmitting} 266 > 267 Cancel 268 </Button> 269 <Button 270 type="button" 271 variant="primary" 272 onClick={(e) => { 273 e.preventDefault(); 274 e.stopPropagation(); 275 handleSubmit(e); 276 }} 277 disabled={isSubmitting} 278 > 279 {isEditMode 280 ? (isUpdating ? "Updating..." : "Update Client") 281 : (isCreating ? "Creating..." : "Register Client")} 282 </Button> 283 </div> 284 </form> 285 </Dialog> 286 ); 287}