diff --git a/apps/web/src/app/(authenticated)/settings/credentials/page.tsx b/apps/web/src/app/(authenticated)/settings/credentials/page.tsx index 73adb5a..e0f3af1 100644 --- a/apps/web/src/app/(authenticated)/settings/credentials/page.tsx +++ b/apps/web/src/app/(authenticated)/settings/credentials/page.tsx @@ -1,18 +1,816 @@ "use client"; -import { useState, useEffect } from "react"; -import { Plus, History } from "lucide-react"; +import { useState, useEffect, type SyntheticEvent } from "react"; +import { Plus, History, Key, RotateCw, Trash2, Eye, EyeOff } from "lucide-react"; import Link from "next/link"; import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; -import { fetchCredentials, type Credential } from "@/lib/api/credentials"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + fetchCredentials, + createCredential, + deleteCredential, + rotateCredential, + CredentialType, + CredentialScope, + type Credential, + type CreateCredentialDto, +} from "@/lib/api/credentials"; import { useWorkspaceId } from "@/lib/hooks"; +/* --------------------------------------------------------------------------- + Constants + --------------------------------------------------------------------------- */ + +const CREDENTIAL_TYPE_LABELS: Record = { + [CredentialType.API_KEY]: "API Key", + [CredentialType.OAUTH_TOKEN]: "OAuth Token", + [CredentialType.ACCESS_TOKEN]: "Access Token", + [CredentialType.SECRET]: "Secret", + [CredentialType.PASSWORD]: "Password", + [CredentialType.CUSTOM]: "Custom", +}; + +const CREDENTIAL_SCOPE_LABELS: Record = { + [CredentialScope.USER]: "User", + [CredentialScope.WORKSPACE]: "Workspace", + [CredentialScope.SYSTEM]: "System", +}; + +/* --------------------------------------------------------------------------- + Add Credential Dialog + --------------------------------------------------------------------------- */ + +interface AddCredentialDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSubmit: (data: CreateCredentialDto) => Promise; + isSubmitting: boolean; +} + +function AddCredentialDialog({ + open, + onOpenChange, + onSubmit, + isSubmitting, +}: AddCredentialDialogProps): React.ReactElement | null { + const [name, setName] = useState(""); + const [provider, setProvider] = useState(""); + const [type, setType] = useState(CredentialType.API_KEY); + const [scope, setScope] = useState(CredentialScope.WORKSPACE); + const [value, setValue] = useState(""); + const [description, setDescription] = useState(""); + const [showValue, setShowValue] = useState(false); + const [formError, setFormError] = useState(null); + + function resetForm(): void { + setName(""); + setProvider(""); + setType(CredentialType.API_KEY); + setScope(CredentialScope.WORKSPACE); + setValue(""); + setDescription(""); + setShowValue(false); + setFormError(null); + } + + async function handleSubmit(e: SyntheticEvent): Promise { + e.preventDefault(); + setFormError(null); + + const trimmedName = name.trim(); + if (!trimmedName) { + setFormError("Name is required."); + return; + } + + const trimmedProvider = provider.trim(); + if (!trimmedProvider) { + setFormError("Provider is required."); + return; + } + + const trimmedValue = value.trim(); + if (!trimmedValue) { + setFormError("Credential value is required."); + return; + } + + try { + const payload: CreateCredentialDto = { + name: trimmedName, + provider: trimmedProvider, + type, + scope, + value: trimmedValue, + }; + + const trimmedDesc = description.trim(); + if (trimmedDesc) { + payload.description = trimmedDesc; + } + + await onSubmit(payload); + resetForm(); + } catch (err: unknown) { + setFormError(err instanceof Error ? err.message : "Failed to create credential."); + } + } + + if (!open) return null; + + return ( +
+ {/* Backdrop */} +
{ + if (!isSubmitting) { + resetForm(); + onOpenChange(false); + } + }} + /> + + {/* Dialog */} +
+

+ Add Credential +

+

+ Securely store an API key, token, or secret. +

+ +
{ + void handleSubmit(e); + }} + > + {/* Name */} +
+ + ) => { + setName(e.target.value); + }} + placeholder="e.g. GitHub Personal Access Token" + maxLength={255} + autoFocus + /> +
+ + {/* Provider */} +
+ + ) => { + setProvider(e.target.value); + }} + placeholder="e.g. github, openai, custom" + maxLength={100} + /> +

+ The service or system this credential belongs to. +

+
+ + {/* Type */} +
+ + +
+ + {/* Scope */} +
+ + +
+ + {/* Value */} +
+ +
+ ) => { + setValue(e.target.value); + }} + placeholder="Paste your secret value here" + style={{ paddingRight: "40px" }} + /> + +
+

+ The value is encrypted at rest and never returned in plaintext. +

+
+ + {/* Description */} +
+ +