import { zodResolver } from "@hookform/resolvers/zod" import { Controller, useForm } from "react-hook-form" import { z } from "zod" import { Button } from "@/components/ui/button" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" import { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import React, { useEffect, useRef, useState } from "react" import { Alert, AlertDescription } from "@/components/ui/alert" import { toast } from "sonner" import { createCredential, updateCredential, getCredentials } from '@/ui/main-axios' import { useTranslation } from "react-i18next" interface Credential { id: number; name: string; description?: string; folder?: string; tags: string[]; authType: 'password' | 'key'; username: string; keyType?: string; usageCount: number; lastUsed?: string; createdAt: string; updatedAt: string; } interface CredentialEditorProps { editingCredential?: Credential | null; onFormSubmit?: () => void; } export function CredentialEditor({ editingCredential, onFormSubmit }: CredentialEditorProps) { const { t } = useTranslation(); const [credentials, setCredentials] = useState([]); const [folders, setFolders] = useState([]); const [loading, setLoading] = useState(true); const [authTab, setAuthTab] = useState<'password' | 'key'>('password'); useEffect(() => { const fetchData = async () => { try { setLoading(true); const credentialsData = await getCredentials(); setCredentials(credentialsData); const uniqueFolders = [...new Set( credentialsData .filter(credential => credential.folder && credential.folder.trim() !== '') .map(credential => credential.folder) )].sort(); setFolders(uniqueFolders); } catch (error) { } finally { setLoading(false); } }; fetchData(); }, []); const formSchema = z.object({ name: z.string().min(1), description: z.string().optional(), folder: z.string().optional(), tags: z.array(z.string().min(1)).default([]), authType: z.enum(['password', 'key']), username: z.string().min(1), password: z.string().optional(), key: z.instanceof(File).optional().nullable(), keyPassword: z.string().optional(), keyType: z.enum([ 'rsa', 'ecdsa', 'ed25519' ]).optional(), }).superRefine((data, ctx) => { if (data.authType === 'password') { if (!data.password || data.password.trim() === '') { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t('credentials.passwordRequired'), path: ['password'] }); } } else if (data.authType === 'key') { if (!data.key && !editingCredential) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: t('credentials.sshKeyRequired'), path: ['key'] }); } } }); type FormData = z.infer; const form = useForm({ resolver: zodResolver(formSchema) as any, defaultValues: { name: editingCredential?.name || "", description: editingCredential?.description || "", folder: editingCredential?.folder || "", tags: editingCredential?.tags || [], authType: editingCredential?.authType || "password", username: editingCredential?.username || "", password: "", key: null, keyPassword: "", keyType: "rsa", } }); useEffect(() => { if (editingCredential) { const defaultAuthType = editingCredential.key ? 'key' : 'password'; setAuthTab(defaultAuthType); form.reset({ name: editingCredential.name || "", description: editingCredential.description || "", folder: editingCredential.folder || "", tags: editingCredential.tags || [], authType: defaultAuthType as 'password' | 'key', username: editingCredential.username || "", password: "", key: null, keyPassword: "", keyType: (editingCredential.keyType as any) || "rsa", }); } else { setAuthTab('password'); form.reset({ name: "", description: "", folder: "", tags: [], authType: "password", username: "", password: "", key: null, keyPassword: "", keyType: "rsa", }); } }, [editingCredential, form]); const onSubmit = async (data: any) => { try { const formData = data as FormData; if (!formData.name || formData.name.trim() === '') { formData.name = formData.username; } if (editingCredential) { await updateCredential(editingCredential.id, formData); toast.success(t('credentials.credentialUpdatedSuccessfully', { name: formData.name })); } else { await createCredential(formData); toast.success(t('credentials.credentialAddedSuccessfully', { name: formData.name })); } if (onFormSubmit) { onFormSubmit(); } window.dispatchEvent(new CustomEvent('credentials:changed')); } catch (error) { toast.error(t('credentials.failedToSaveCredential')); } }; const [tagInput, setTagInput] = useState(""); const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); const folderInputRef = useRef(null); const folderDropdownRef = useRef(null); const folderValue = form.watch('folder'); const filteredFolders = React.useMemo(() => { if (!folderValue) return folders; return folders.filter(f => f.toLowerCase().includes(folderValue.toLowerCase())); }, [folderValue, folders]); const handleFolderClick = (folder: string) => { form.setValue('folder', folder); setFolderDropdownOpen(false); }; useEffect(() => { function handleClickOutside(event: MouseEvent) { if ( folderDropdownRef.current && !folderDropdownRef.current.contains(event.target as Node) && folderInputRef.current && !folderInputRef.current.contains(event.target as Node) ) { setFolderDropdownOpen(false); } } if (folderDropdownOpen) { document.addEventListener('mousedown', handleClickOutside); } else { document.removeEventListener('mousedown', handleClickOutside); } return () => { document.removeEventListener('mousedown', handleClickOutside); }; }, [folderDropdownOpen]); const keyTypeOptions = [ { value: 'rsa', label: t('credentials.keyTypeRSA') }, { value: 'ecdsa', label: t('credentials.keyTypeECDSA') }, { value: 'ed25519', label: t('credentials.keyTypeEd25519') }, ]; const [keyTypeDropdownOpen, setKeyTypeDropdownOpen] = useState(false); const keyTypeButtonRef = useRef(null); const keyTypeDropdownRef = useRef(null); useEffect(() => { function onClickOutside(event: MouseEvent) { if ( keyTypeDropdownOpen && keyTypeDropdownRef.current && !keyTypeDropdownRef.current.contains(event.target as Node) && keyTypeButtonRef.current && !keyTypeButtonRef.current.contains(event.target as Node) ) { setKeyTypeDropdownOpen(false); } } document.addEventListener("mousedown", onClickOutside); return () => document.removeEventListener("mousedown", onClickOutside); }, [keyTypeDropdownOpen]); return (
{t('credentials.general')} {t('credentials.authentication')} {t('credentials.basicInformation')}
( {t('credentials.credentialName')} )} /> ( {t('credentials.username')} )} />
{t('credentials.organization')}
( {t('credentials.description')} )} /> ( {t('credentials.folder')} setFolderDropdownOpen(true)} onChange={e => { field.onChange(e); setFolderDropdownOpen(true); }} /> {folderDropdownOpen && filteredFolders.length > 0 && (
{filteredFolders.map((folder) => ( ))}
)}
)} /> ( {t('credentials.tags')}
{field.value.map((tag: string, idx: number) => ( {tag} ))} setTagInput(e.target.value)} onKeyDown={e => { if (e.key === " " && tagInput.trim() !== "") { e.preventDefault(); if (!field.value.includes(tagInput.trim())) { field.onChange([...field.value, tagInput.trim()]); } setTagInput(""); } else if (e.key === "Backspace" && tagInput === "" && field.value.length > 0) { field.onChange(field.value.slice(0, -1)); } }} placeholder={t('credentials.addTagsSpaceToAdd')} />
)} />
{t('credentials.authentication')} { setAuthTab(value as 'password' | 'key'); form.setValue('authType', value as 'password' | 'key'); // Clear other auth fields when switching if (value === 'password') { form.setValue('key', null); form.setValue('keyPassword', ''); } else if (value === 'key') { form.setValue('password', ''); } }} className="flex-1 flex flex-col h-full min-h-0" > {t('credentials.password')} {t('credentials.key')} ( {t('credentials.password')} )} />
( {t('credentials.sshPrivateKey')}
{ const file = e.target.files?.[0]; field.onChange(file || null); }} className="absolute inset-0 w-full h-full opacity-0 cursor-pointer" />
)} /> ( {t('credentials.keyPassword')} )} /> ( {t('credentials.keyType')}
{keyTypeDropdownOpen && (
{keyTypeOptions.map((opt) => ( ))}
)}
)} />
); }