Credential manager + bug fixes #191
@@ -12,7 +12,12 @@
|
|||||||
"Bash(npm run build:*)",
|
"Bash(npm run build:*)",
|
||||||
"Bash(npm install)",
|
"Bash(npm install)",
|
||||||
"Bash(npm run electron:build:*)",
|
"Bash(npm run electron:build:*)",
|
||||||
"Bash(npm uninstall:*)"
|
"Bash(npm uninstall:*)",
|
||||||
|
"Bash(git remote set-url:*)",
|
||||||
|
"Bash(npm run dev:backend:*)",
|
||||||
|
"Bash(taskkill:*)",
|
||||||
|
"Bash(node:*)",
|
||||||
|
"WebFetch(domain:ui.shadcn.com)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ function startBackendServer() {
|
|||||||
|
|
||||||
backendProcess = spawn('node', [backendPath], {
|
backendProcess = spawn('node', [backendPath], {
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
detached: false
|
detached: false,
|
||||||
|
cwd: path.join(__dirname, '..') // Set working directory to app root
|
||||||
});
|
});
|
||||||
|
|
||||||
backendProcess.stdout.on('data', (data) => {
|
backendProcess.stdout.on('data', (data) => {
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"required": "Required",
|
"required": "Required",
|
||||||
"optional": "Optional",
|
"optional": "Optional",
|
||||||
|
"clear": "Clear",
|
||||||
"toggleSidebar": "Toggle Sidebar",
|
"toggleSidebar": "Toggle Sidebar",
|
||||||
"sidebar": "Sidebar",
|
"sidebar": "Sidebar",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
@@ -376,6 +377,10 @@
|
|||||||
"authentication": "Authentication",
|
"authentication": "Authentication",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"key": "Key",
|
"key": "Key",
|
||||||
|
"credential": "Credential",
|
||||||
|
"selectCredential": "Select Credential",
|
||||||
|
"selectCredentialPlaceholder": "Choose a credential...",
|
||||||
|
"credentialRequired": "Credential is required when using credential authentication",
|
||||||
"sshPrivateKey": "SSH Private Key",
|
"sshPrivateKey": "SSH Private Key",
|
||||||
"keyPassword": "Key Password",
|
"keyPassword": "Key Password",
|
||||||
"keyType": "Key Type",
|
"keyType": "Key Type",
|
||||||
|
|||||||
@@ -131,6 +131,7 @@
|
|||||||
"loading": "加载中",
|
"loading": "加载中",
|
||||||
"required": "必填",
|
"required": "必填",
|
||||||
"optional": "可选",
|
"optional": "可选",
|
||||||
|
"clear": "清除",
|
||||||
"toggleSidebar": "切换侧边栏",
|
"toggleSidebar": "切换侧边栏",
|
||||||
"sidebar": "侧边栏",
|
"sidebar": "侧边栏",
|
||||||
"home": "首页",
|
"home": "首页",
|
||||||
@@ -397,6 +398,10 @@
|
|||||||
"authentication": "认证方式",
|
"authentication": "认证方式",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"key": "密钥",
|
"key": "密钥",
|
||||||
|
"credential": "凭证",
|
||||||
|
"selectCredential": "选择凭证",
|
||||||
|
"selectCredentialPlaceholder": "选择一个凭证...",
|
||||||
|
"credentialRequired": "使用凭证认证时需要选择凭证",
|
||||||
"sshPrivateKey": "SSH 私钥",
|
"sshPrivateKey": "SSH 私钥",
|
||||||
"keyPassword": "密钥密码",
|
"keyPassword": "密钥密码",
|
||||||
"keyType": "密钥类型",
|
"keyType": "密钥类型",
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ router.get('/oidc-config', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const row = db.$client.prepare("SELECT value FROM settings WHERE key = 'oidc_config'").get();
|
const row = db.$client.prepare("SELECT value FROM settings WHERE key = 'oidc_config'").get();
|
||||||
if (!row) {
|
if (!row) {
|
||||||
return res.status(404).json({error: 'OIDC not configured'});
|
return res.json(null);
|
||||||
}
|
}
|
||||||
res.json(JSON.parse((row as any).value));
|
res.json(JSON.parse((row as any).value));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { FormControl, FormItem, FormLabel } from "@/components/ui/form";
|
||||||
|
import { getCredentials } from '@/ui/main-axios';
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
interface Credential {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
username: string;
|
||||||
|
authType: 'password' | 'key';
|
||||||
|
folder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CredentialSelectorProps {
|
||||||
|
value?: number | null;
|
||||||
|
onValueChange: (credentialId: number | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CredentialSelector({ value, onValueChange }: CredentialSelectorProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [credentials, setCredentials] = useState<Credential[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchCredentials = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const data = await getCredentials();
|
||||||
|
setCredentials(data.credentials || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch credentials:', error);
|
||||||
|
setCredentials([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchCredentials();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (
|
||||||
|
dropdownRef.current &&
|
||||||
|
!dropdownRef.current.contains(event.target as Node) &&
|
||||||
|
buttonRef.current &&
|
||||||
|
!buttonRef.current.contains(event.target as Node)
|
||||||
|
) {
|
||||||
|
setDropdownOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropdownOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [dropdownOpen]);
|
||||||
|
|
||||||
|
const selectedCredential = credentials.find(c => c.id === value);
|
||||||
|
|
||||||
|
const filteredCredentials = credentials.filter(credential => {
|
||||||
|
if (!searchQuery) return true;
|
||||||
|
const searchLower = searchQuery.toLowerCase();
|
||||||
|
return (
|
||||||
|
credential.name.toLowerCase().includes(searchLower) ||
|
||||||
|
credential.username.toLowerCase().includes(searchLower) ||
|
||||||
|
(credential.folder && credential.folder.toLowerCase().includes(searchLower))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleCredentialSelect = (credential: Credential) => {
|
||||||
|
onValueChange(credential.id);
|
||||||
|
setDropdownOpen(false);
|
||||||
|
setSearchQuery('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClear = () => {
|
||||||
|
onValueChange(null);
|
||||||
|
setDropdownOpen(false);
|
||||||
|
setSearchQuery('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>{t('hosts.selectCredential')}</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<div className="relative">
|
||||||
|
<Button
|
||||||
|
ref={buttonRef}
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-between text-left rounded-md px-3 py-2 bg-[#18181b] border border-input text-foreground"
|
||||||
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
t('common.loading')
|
||||||
|
) : selectedCredential ? (
|
||||||
|
<div className="flex items-center justify-between w-full">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium">{selectedCredential.name}</span>
|
||||||
|
<span className="text-sm text-muted-foreground ml-2">
|
||||||
|
({selectedCredential.username} • {selectedCredential.authType})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
t('hosts.selectCredentialPlaceholder')
|
||||||
|
)}
|
||||||
|
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{dropdownOpen && (
|
||||||
|
<div
|
||||||
|
ref={dropdownRef}
|
||||||
|
className="absolute top-full left-0 z-50 mt-1 w-full bg-[#18181b] border border-input rounded-md shadow-lg max-h-80 overflow-hidden"
|
||||||
|
>
|
||||||
|
<div className="p-2 border-b border-input">
|
||||||
|
<Input
|
||||||
|
placeholder={t('credentials.searchCredentials')}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="h-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-60 overflow-y-auto p-1">
|
||||||
|
{loading ? (
|
||||||
|
<div className="p-3 text-center text-sm text-muted-foreground">
|
||||||
|
{t('common.loading')}
|
||||||
|
</div>
|
||||||
|
) : filteredCredentials.length === 0 ? (
|
||||||
|
<div className="p-3 text-center text-sm text-muted-foreground">
|
||||||
|
{searchQuery ? t('credentials.noCredentialsMatchFilters') : t('credentials.noCredentialsYet')}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-1">
|
||||||
|
{value && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="w-full justify-start text-left rounded-md px-2 py-2 text-red-400 hover:bg-red-500/20"
|
||||||
|
onClick={handleClear}
|
||||||
|
>
|
||||||
|
{t('common.clear')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{filteredCredentials.map((credential) => (
|
||||||
|
<Button
|
||||||
|
key={credential.id}
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={`w-full justify-start text-left rounded-md px-2 py-2 hover:bg-white/15 focus:bg-white/20 focus:outline-none ${
|
||||||
|
credential.id === value ? 'bg-white/20' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => handleCredentialSelect(credential)}
|
||||||
|
>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="font-medium">{credential.name}</span>
|
||||||
|
{credential.folder && (
|
||||||
|
<span className="text-xs bg-muted px-1 rounded">
|
||||||
|
{credential.folder}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
{credential.username} • {credential.authType}
|
||||||
|
{credential.description && ` • ${credential.description}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -58,9 +58,9 @@ function SheetContent({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=closed]:pointer-events-none",
|
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=closed]:pointer-events-none",
|
||||||
side === "right" &&
|
side === "right" &&
|
||||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm",
|
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l",
|
||||||
side === "left" &&
|
side === "left" &&
|
||||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm",
|
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r",
|
||||||
side === "top" &&
|
side === "top" &&
|
||||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||||
side === "bottom" &&
|
side === "bottom" &&
|
||||||
|
|||||||
@@ -79,7 +79,9 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
.then(res => {
|
.then(res => {
|
||||||
if (res) setOidcConfig(res);
|
if (res) setOidcConfig(res);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(error => {
|
||||||
|
// Silently ignore OIDC config fetch errors - this is expected when OIDC is not configured
|
||||||
|
console.debug('OIDC config not available:', error.message);
|
||||||
});
|
});
|
||||||
fetchUsers();
|
fetchUsers();
|
||||||
}, []);
|
}, []);
|
||||||
@@ -370,7 +372,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
<TableCell className="px-4">
|
<TableCell className="px-4">
|
||||||
<Button variant="ghost" size="sm"
|
<Button variant="ghost" size="sm"
|
||||||
onClick={() => handleDeleteUser(user.username)}
|
onClick={() => handleDeleteUser(user.username)}
|
||||||
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
className="text-red-600 hover:text-red-700 hover:bg-red-900/20 dark:hover:bg-red-900/30"
|
||||||
disabled={user.is_admin}>
|
disabled={user.is_admin}>
|
||||||
<Trash2 className="h-4 w-4"/>
|
<Trash2 className="h-4 w-4"/>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -434,7 +436,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
|
|||||||
<TableCell className="px-4">
|
<TableCell className="px-4">
|
||||||
<Button variant="ghost" size="sm"
|
<Button variant="ghost" size="sm"
|
||||||
onClick={() => handleRemoveAdminStatus(admin.username)}
|
onClick={() => handleRemoveAdminStatus(admin.username)}
|
||||||
className="text-orange-600 hover:text-orange-700 hover:bg-orange-50">
|
className="text-orange-600 hover:text-orange-700 hover:bg-orange-900/20 dark:hover:bg-orange-900/30">
|
||||||
<Shield className="h-4 w-4"/>
|
<Shield className="h-4 w-4"/>
|
||||||
Remove Admin
|
Remove Admin
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -211,15 +211,15 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={true} onOpenChange={onCancel}>
|
<Sheet open={true} onOpenChange={onCancel}>
|
||||||
<SheetContent className="w-[800px] max-w-[90vw] overflow-y-auto">
|
<SheetContent className="w-[600px] max-w-[50vw] overflow-y-auto">
|
||||||
<SheetHeader>
|
<SheetHeader className="space-y-4 pb-8">
|
||||||
<SheetTitle className="flex items-center space-x-2">
|
<SheetTitle className="flex items-center space-x-3">
|
||||||
<Key className="h-5 w-5 text-blue-600" />
|
<Key className="h-6 w-6 text-zinc-600 dark:text-zinc-400" />
|
||||||
<span>
|
<span className="text-xl font-semibold">
|
||||||
{credential ? t('credentials.editCredential') : t('credentials.createCredential')}
|
{credential ? t('credentials.editCredential') : t('credentials.createCredential')}
|
||||||
</span>
|
</span>
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription className="text-base text-zinc-600 dark:text-zinc-400">
|
||||||
{credential
|
{credential
|
||||||
? t('credentials.editCredentialDescription')
|
? t('credentials.editCredentialDescription')
|
||||||
: t('credentials.createCredentialDescription')
|
: t('credentials.createCredentialDescription')
|
||||||
@@ -227,26 +227,26 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
<form onSubmit={handleSubmit} className="space-y-10 px-2">
|
||||||
<Tabs defaultValue="basic" className="w-full">
|
<Tabs defaultValue="basic" className="w-full">
|
||||||
<TabsList className="grid w-full grid-cols-3 bg-[#18181b] border-2 border-[#303032]">
|
<TabsList className="grid w-full grid-cols-3 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700">
|
||||||
<TabsTrigger value="basic">{t('credentials.basicInfo')}</TabsTrigger>
|
<TabsTrigger value="basic">{t('credentials.basicInfo')}</TabsTrigger>
|
||||||
<TabsTrigger value="auth">{t('credentials.authentication')}</TabsTrigger>
|
<TabsTrigger value="auth">{t('credentials.authentication')}</TabsTrigger>
|
||||||
<TabsTrigger value="organization">{t('credentials.organization')}</TabsTrigger>
|
<TabsTrigger value="organization">{t('credentials.organization')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="basic" className="space-y-4">
|
<TabsContent value="basic" className="space-y-8 mt-8">
|
||||||
<Card>
|
<Card className="border-zinc-200 dark:border-zinc-700">
|
||||||
<CardHeader>
|
<CardHeader className="pb-8">
|
||||||
<CardTitle className="text-lg">{t('credentials.basicInformation')}</CardTitle>
|
<CardTitle className="text-lg font-semibold">{t('credentials.basicInformation')}</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription className="text-zinc-600 dark:text-zinc-400">
|
||||||
{t('credentials.basicInformationDescription')}
|
{t('credentials.basicInformationDescription')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-8">
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="name" className="flex items-center space-x-1">
|
<Label htmlFor="name" className="flex items-center space-x-2 text-sm font-medium">
|
||||||
<User className="h-4 w-4" />
|
<User className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
|
||||||
<span>{t('credentials.credentialName')}</span>
|
<span>{t('credentials.credentialName')}</span>
|
||||||
<span className="text-red-500">*</span>
|
<span className="text-red-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
@@ -265,7 +265,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="description">{t('credentials.credentialDescription')}</Label>
|
<Label htmlFor="description">{t('credentials.credentialDescription')}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="description"
|
id="description"
|
||||||
@@ -276,7 +276,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="username" className="flex items-center space-x-1">
|
<Label htmlFor="username" className="flex items-center space-x-1">
|
||||||
<User className="h-4 w-4" />
|
<User className="h-4 w-4" />
|
||||||
<span>{t('common.username')}</span>
|
<span>{t('common.username')}</span>
|
||||||
@@ -300,56 +300,56 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="auth" className="space-y-4">
|
<TabsContent value="auth" className="space-y-6 mt-8">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg flex items-center space-x-2">
|
<CardTitle className="text-lg flex items-center space-x-2">
|
||||||
<Shield className="h-5 w-5 text-green-600" />
|
<Shield className="h-5 w-5 text-zinc-600 dark:text-zinc-400" />
|
||||||
<span>{t('credentials.authenticationMethod')}</span>
|
<span>{t('credentials.authenticationMethod')}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t('credentials.authenticationMethodDescription')}
|
{t('credentials.authenticationMethodDescription')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-6">
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<Label>{t('credentials.authenticationType')}</Label>
|
<Label>{t('credentials.authenticationType')}</Label>
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-6">
|
||||||
<div
|
<div
|
||||||
className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-colors ${
|
className={`flex-1 p-6 rounded-lg border-2 cursor-pointer transition-colors ${
|
||||||
formData.authType === 'password'
|
formData.authType === 'password'
|
||||||
? 'border-blue-500 bg-blue-900/20 dark:bg-blue-900/20'
|
? 'border-zinc-500 bg-zinc-900/20 dark:bg-zinc-900/20'
|
||||||
: 'border-gray-600 hover:border-gray-500 dark:border-gray-600 dark:hover:border-gray-500'
|
: 'border-zinc-600 hover:border-zinc-500 dark:border-zinc-600 dark:hover:border-zinc-500'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setFormData(prev => ({ ...prev, authType: 'password' }))}
|
onClick={() => setFormData(prev => ({ ...prev, authType: 'password' }))}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-4">
|
||||||
<Lock className="h-5 w-5 text-orange-500" />
|
<Lock className="h-5 w-5 text-zinc-600 dark:text-zinc-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{t('common.password')}</div>
|
<div className="font-medium">{t('common.password')}</div>
|
||||||
<div className="text-sm text-gray-500">{t('credentials.passwordAuthDescription')}</div>
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.passwordAuthDescription')}</div>
|
||||||
</div>
|
</div>
|
||||||
{formData.authType === 'password' && (
|
{formData.authType === 'password' && (
|
||||||
<Check className="h-5 w-5 text-blue-500" />
|
<Check className="h-5 w-5 text-zinc-500" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`flex-1 p-4 rounded-lg border-2 cursor-pointer transition-colors ${
|
className={`flex-1 p-6 rounded-lg border-2 cursor-pointer transition-colors ${
|
||||||
formData.authType === 'key'
|
formData.authType === 'key'
|
||||||
? 'border-green-500 bg-green-900/20 dark:bg-green-900/20'
|
? 'border-zinc-500 bg-zinc-900/20 dark:bg-zinc-900/20'
|
||||||
: 'border-gray-600 hover:border-gray-500 dark:border-gray-600 dark:hover:border-gray-500'
|
: 'border-zinc-600 hover:border-zinc-500 dark:border-zinc-600 dark:hover:border-zinc-500'
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setFormData(prev => ({ ...prev, authType: 'key' }))}
|
onClick={() => setFormData(prev => ({ ...prev, authType: 'key' }))}
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-4">
|
||||||
<Key className="h-5 w-5 text-green-500" />
|
<Key className="h-5 w-5 text-zinc-500 dark:text-zinc-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">{t('credentials.sshKey')}</div>
|
<div className="font-medium">{t('credentials.sshKey')}</div>
|
||||||
<div className="text-sm text-gray-500">{t('credentials.sshKeyAuthDescription')}</div>
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.sshKeyAuthDescription')}</div>
|
||||||
</div>
|
</div>
|
||||||
{formData.authType === 'key' && (
|
{formData.authType === 'key' && (
|
||||||
<Check className="h-5 w-5 text-green-500" />
|
<Check className="h-5 w-5 text-zinc-500 dark:text-zinc-400" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -359,7 +359,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{formData.authType === 'password' && (
|
{formData.authType === 'password' && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="password" className="flex items-center space-x-1">
|
<Label htmlFor="password" className="flex items-center space-x-1">
|
||||||
<Lock className="h-4 w-4" />
|
<Lock className="h-4 w-4" />
|
||||||
<span>{t('common.password')}</span>
|
<span>{t('common.password')}</span>
|
||||||
@@ -394,8 +394,8 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{formData.authType === 'key' && (
|
{formData.authType === 'key' && (
|
||||||
|
<div className="space-y-6">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="flex items-center space-x-1">
|
<Label className="flex items-center space-x-1">
|
||||||
<Key className="h-4 w-4" />
|
<Key className="h-4 w-4" />
|
||||||
<span>{t('credentials.sshKeyType')}</span>
|
<span>{t('credentials.sshKeyType')}</span>
|
||||||
@@ -412,7 +412,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="key" className="flex items-center space-x-1">
|
<Label htmlFor="key" className="flex items-center space-x-1">
|
||||||
<Key className="h-4 w-4" />
|
<Key className="h-4 w-4" />
|
||||||
<span>{t('credentials.privateKey')}</span>
|
<span>{t('credentials.privateKey')}</span>
|
||||||
@@ -432,7 +432,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
<span>{errors.key}</span>
|
<span>{errors.key}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-3">
|
||||||
<Button type="button" variant="outline" size="sm" onClick={() => document.getElementById('key-file')?.click()}>
|
<Button type="button" variant="outline" size="sm" onClick={() => document.getElementById('key-file')?.click()}>
|
||||||
<Upload className="h-4 w-4 mr-1" />
|
<Upload className="h-4 w-4 mr-1" />
|
||||||
{t('credentials.uploadKeyFile')}
|
{t('credentials.uploadKeyFile')}
|
||||||
@@ -451,7 +451,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="keyPassword">{t('credentials.keyPassphrase')}</Label>
|
<Label htmlFor="keyPassword">{t('credentials.keyPassphrase')}</Label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
@@ -472,7 +472,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
{showKeyPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
{showKeyPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-gray-500">{t('credentials.keyPassphraseOptional')}</p>
|
<p className="text-xs text-zinc-500 dark:text-zinc-400">{t('credentials.keyPassphraseOptional')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -480,29 +480,29 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="organization" className="space-y-4">
|
<TabsContent value="organization" className="space-y-6 mt-8">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg flex items-center space-x-2">
|
<CardTitle className="text-lg flex items-center space-x-2">
|
||||||
<Folder className="h-5 w-5 text-amber-500" />
|
<Folder className="h-5 w-5 text-zinc-500 dark:text-zinc-400" />
|
||||||
<span>{t('credentials.organization')}</span>
|
<span>{t('credentials.organization')}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{t('credentials.organizationDescription')}
|
{t('credentials.organizationDescription')}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label htmlFor="folder" className="flex items-center space-x-1">
|
<Label htmlFor="folder" className="flex items-center space-x-1">
|
||||||
<Folder className="h-4 w-4" />
|
<Folder className="h-4 w-4" />
|
||||||
<span>{t('common.folder')}</span>
|
<span>{t('common.folder')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select value={formData.folder} onValueChange={(value) => setFormData(prev => ({ ...prev, folder: value }))}>
|
<Select value={formData.folder || "__none__"} onValueChange={(value) => setFormData(prev => ({ ...prev, folder: value === "__none__" ? "" : value }))}>
|
||||||
<SelectTrigger>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder={t('credentials.selectOrCreateFolder')} />
|
<SelectValue placeholder={t('credentials.selectOrCreateFolder')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="">{t('credentials.noFolder')}</SelectItem>
|
<SelectItem value="__none__">{t('credentials.noFolder')}</SelectItem>
|
||||||
{existingFolders.map(folder => (
|
{existingFolders.map(folder => (
|
||||||
<SelectItem key={folder} value={folder}>{folder}</SelectItem>
|
<SelectItem key={folder} value={folder}>{folder}</SelectItem>
|
||||||
))}
|
))}
|
||||||
@@ -515,12 +515,12 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-4">
|
||||||
<Label className="flex items-center space-x-1">
|
<Label className="flex items-center space-x-1">
|
||||||
<Tag className="h-4 w-4" />
|
<Tag className="h-4 w-4" />
|
||||||
<span>{t('hosts.tags')}</span>
|
<span>{t('hosts.tags')}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex flex-wrap gap-2 mb-2">
|
<div className="flex flex-wrap gap-3 mb-4">
|
||||||
{formData.tags.map((tag, index) => (
|
{formData.tags.map((tag, index) => (
|
||||||
<Badge key={index} variant="secondary" className="pr-1">
|
<Badge key={index} variant="secondary" className="pr-1">
|
||||||
{tag}
|
{tag}
|
||||||
@@ -536,7 +536,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-3">
|
||||||
<Input
|
<Input
|
||||||
placeholder={t('credentials.addTag')}
|
placeholder={t('credentials.addTag')}
|
||||||
value={newTag}
|
value={newTag}
|
||||||
@@ -558,17 +558,12 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<SheetFooter className="flex justify-between">
|
<SheetFooter className="flex justify-end items-center pt-8 border-t border-zinc-200 dark:border-zinc-700">
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-4">
|
||||||
<Button type="button" variant="outline" onClick={testConnection}>
|
<Button type="button" variant="outline" size="lg" onClick={onCancel} className="border-zinc-300 dark:border-zinc-600">
|
||||||
{t('credentials.testConnection')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Button type="button" variant="outline" onClick={onCancel}>
|
|
||||||
{t('common.cancel')}
|
{t('common.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="submit" disabled={saving}>
|
<Button type="submit" size="lg" disabled={saving}>
|
||||||
{saving ? t('credentials.saving') : credential ? t('credentials.updateCredential') : t('credentials.createCredential')}
|
{saving ? t('credentials.saving') : credential ? t('credentials.updateCredential') : t('credentials.createCredential')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -120,9 +120,9 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
|
|
||||||
const getAuthIcon = (authType: string) => {
|
const getAuthIcon = (authType: string) => {
|
||||||
return authType === 'password' ? (
|
return authType === 'password' ? (
|
||||||
<Key className="h-5 w-5 text-orange-500" />
|
<Key className="h-5 w-5 text-zinc-600 dark:text-zinc-400" />
|
||||||
) : (
|
) : (
|
||||||
<Shield className="h-5 w-5 text-green-500" />
|
<Shield className="h-5 w-5 text-zinc-500 dark:text-zinc-400" />
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,7 +139,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
<label className="text-sm font-medium text-zinc-700 dark:text-zinc-300">
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -159,13 +159,13 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`p-3 rounded-md bg-gray-800 dark:bg-gray-800 ${isMultiline ? '' : 'min-h-[2.5rem]'}`}>
|
<div className={`p-3 rounded-md bg-zinc-800 dark:bg-zinc-800 ${isMultiline ? '' : 'min-h-[2.5rem]'}`}>
|
||||||
{isVisible ? (
|
{isVisible ? (
|
||||||
<pre className={`text-sm ${isMultiline ? 'whitespace-pre-wrap' : 'whitespace-nowrap'} font-mono`}>
|
<pre className={`text-sm ${isMultiline ? 'whitespace-pre-wrap' : 'whitespace-nowrap'} font-mono`}>
|
||||||
{value}
|
{value}
|
||||||
</pre>
|
</pre>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
{'•'.repeat(isMultiline ? 50 : 20)}
|
{'•'.repeat(isMultiline ? 50 : 20)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -177,9 +177,9 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
if (loading || !credentialDetails) {
|
if (loading || !credentialDetails) {
|
||||||
return (
|
return (
|
||||||
<Sheet open={true} onOpenChange={onClose}>
|
<Sheet open={true} onOpenChange={onClose}>
|
||||||
<SheetContent className="w-[800px] max-w-[90vw]">
|
<SheetContent className="w-[600px] max-w-[50vw]">
|
||||||
<div className="flex items-center justify-center h-64">
|
<div className="flex items-center justify-center h-64">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-zinc-600"></div>
|
||||||
</div>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
@@ -188,35 +188,39 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={true} onOpenChange={onClose}>
|
<Sheet open={true} onOpenChange={onClose}>
|
||||||
<SheetContent className="w-[800px] max-w-[90vw] overflow-y-auto">
|
<SheetContent className="w-[600px] max-w-[50vw] overflow-y-auto">
|
||||||
<SheetHeader>
|
<SheetHeader className="space-y-6 pb-8">
|
||||||
<SheetTitle className="flex items-center space-x-3">
|
<SheetTitle className="flex items-center space-x-4">
|
||||||
|
<div className="p-2 rounded-lg bg-zinc-100 dark:bg-zinc-800">
|
||||||
{getAuthIcon(credentialDetails.authType)}
|
{getAuthIcon(credentialDetails.authType)}
|
||||||
<div>
|
</div>
|
||||||
<div>{credentialDetails.name}</div>
|
<div className="flex-1">
|
||||||
<div className="text-sm font-normal text-gray-600 dark:text-gray-400">
|
<div className="text-xl font-semibold">{credentialDetails.name}</div>
|
||||||
|
<div className="text-sm font-normal text-zinc-600 dark:text-zinc-400 mt-1">
|
||||||
{credentialDetails.description}
|
{credentialDetails.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2 ml-auto">
|
<div className="flex items-center space-x-2">
|
||||||
<Badge variant={credentialDetails.authType === 'password' ? 'secondary' : 'outline'}>
|
<Badge variant="outline" className="border-zinc-300 dark:border-zinc-600 text-zinc-600 dark:text-zinc-400">
|
||||||
{credentialDetails.authType}
|
{credentialDetails.authType}
|
||||||
</Badge>
|
</Badge>
|
||||||
{credentialDetails.keyType && (
|
{credentialDetails.keyType && (
|
||||||
<Badge variant="outline">{credentialDetails.keyType}</Badge>
|
<Badge variant="secondary" className="bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300">
|
||||||
|
{credentialDetails.keyType}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</SheetTitle>
|
</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-10">
|
||||||
{/* Tab Navigation */}
|
{/* Tab Navigation */}
|
||||||
<div className="flex space-x-1 p-1 bg-[#18181b] border-2 border-[#303032] rounded-lg">
|
<div className="flex space-x-2 p-2 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg">
|
||||||
<Button
|
<Button
|
||||||
variant={activeTab === 'overview' ? 'default' : 'ghost'}
|
variant={activeTab === 'overview' ? 'default' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setActiveTab('overview')}
|
onClick={() => setActiveTab('overview')}
|
||||||
className="flex-1"
|
className="flex-1 h-10"
|
||||||
>
|
>
|
||||||
<FileText className="h-4 w-4 mr-2" />
|
<FileText className="h-4 w-4 mr-2" />
|
||||||
{t('credentials.overview')}
|
{t('credentials.overview')}
|
||||||
@@ -225,7 +229,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
variant={activeTab === 'security' ? 'default' : 'ghost'}
|
variant={activeTab === 'security' ? 'default' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setActiveTab('security')}
|
onClick={() => setActiveTab('security')}
|
||||||
className="flex-1"
|
className="flex-1 h-10"
|
||||||
>
|
>
|
||||||
<Shield className="h-4 w-4 mr-2" />
|
<Shield className="h-4 w-4 mr-2" />
|
||||||
{t('credentials.security')}
|
{t('credentials.security')}
|
||||||
@@ -234,7 +238,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
variant={activeTab === 'usage' ? 'default' : 'ghost'}
|
variant={activeTab === 'usage' ? 'default' : 'ghost'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setActiveTab('usage')}
|
onClick={() => setActiveTab('usage')}
|
||||||
className="flex-1"
|
className="flex-1 h-10"
|
||||||
>
|
>
|
||||||
<Server className="h-4 w-4 mr-2" />
|
<Server className="h-4 w-4 mr-2" />
|
||||||
{t('credentials.usage')}
|
{t('credentials.usage')}
|
||||||
@@ -243,36 +247,38 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
|
|
||||||
{/* Tab Content */}
|
{/* Tab Content */}
|
||||||
{activeTab === 'overview' && (
|
{activeTab === 'overview' && (
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-10 lg:grid-cols-2">
|
||||||
<Card>
|
<Card className="border-zinc-200 dark:border-zinc-700">
|
||||||
<CardHeader>
|
<CardHeader className="pb-8">
|
||||||
<CardTitle className="text-lg">{t('credentials.basicInformation')}</CardTitle>
|
<CardTitle className="text-lg font-semibold">{t('credentials.basicInformation')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-8">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-5">
|
||||||
<User className="h-4 w-4 text-gray-500" />
|
<div className="p-2 rounded-lg bg-zinc-100 dark:bg-zinc-800">
|
||||||
|
<User className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-500">{t('common.username')}</div>
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('common.username')}</div>
|
||||||
<div className="font-medium">{credentialDetails.username}</div>
|
<div className="font-medium text-zinc-800 dark:text-zinc-200">{credentialDetails.username}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{credentialDetails.folder && (
|
{credentialDetails.folder && (
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-4">
|
||||||
<Folder className="h-4 w-4 text-gray-500" />
|
<Folder className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-500">{t('common.folder')}</div>
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('common.folder')}</div>
|
||||||
<div className="font-medium">{credentialDetails.folder}</div>
|
<div className="font-medium">{credentialDetails.folder}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{credentialDetails.tags.length > 0 && (
|
{credentialDetails.tags.length > 0 && (
|
||||||
<div className="flex items-start space-x-3">
|
<div className="flex items-start space-x-4">
|
||||||
<Hash className="h-4 w-4 text-gray-500 mt-1" />
|
<Hash className="h-4 w-4 text-zinc-500 dark:text-zinc-400 mt-1" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="text-sm text-gray-500 mb-2">{t('hosts.tags')}</div>
|
<div className="text-sm text-zinc-500 dark:text-zinc-400 mb-3">{t('hosts.tags')}</div>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-2">
|
||||||
{credentialDetails.tags.map((tag, index) => (
|
{credentialDetails.tags.map((tag, index) => (
|
||||||
<Badge key={index} variant="outline" className="text-xs">
|
<Badge key={index} variant="outline" className="text-xs">
|
||||||
{tag}
|
{tag}
|
||||||
@@ -285,18 +291,18 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-4">
|
||||||
<Calendar className="h-4 w-4 text-gray-500" />
|
<Calendar className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-500">{t('credentials.created')}</div>
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.created')}</div>
|
||||||
<div className="font-medium">{formatDate(credentialDetails.createdAt)}</div>
|
<div className="font-medium">{formatDate(credentialDetails.createdAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-4">
|
||||||
<Calendar className="h-4 w-4 text-gray-500" />
|
<Calendar className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-500">{t('credentials.lastModified')}</div>
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.lastModified')}</div>
|
||||||
<div className="font-medium">{formatDate(credentialDetails.updatedAt)}</div>
|
<div className="font-medium">{formatDate(credentialDetails.updatedAt)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -307,30 +313,30 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg">{t('credentials.usageStatistics')}</CardTitle>
|
<CardTitle className="text-lg">{t('credentials.usageStatistics')}</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-6">
|
||||||
<div className="text-center p-4 bg-blue-900/20 dark:bg-blue-900/20 rounded-lg">
|
<div className="text-center p-6 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
||||||
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
<div className="text-3xl font-bold text-zinc-600 dark:text-zinc-400">
|
||||||
{credentialDetails.usageCount}
|
{credentialDetails.usageCount}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
<div className="text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
{t('credentials.timesUsed')}
|
{t('credentials.timesUsed')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{credentialDetails.lastUsed && (
|
{credentialDetails.lastUsed && (
|
||||||
<div className="flex items-center space-x-3 p-3 bg-green-900/20 dark:bg-green-900/20 rounded-lg">
|
<div className="flex items-center space-x-4 p-4 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
||||||
<Clock className="h-5 w-5 text-green-600 dark:text-green-400" />
|
<Clock className="h-5 w-5 text-zinc-600 dark:text-zinc-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-500">{t('credentials.lastUsed')}</div>
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.lastUsed')}</div>
|
||||||
<div className="font-medium">{formatDate(credentialDetails.lastUsed)}</div>
|
<div className="font-medium">{formatDate(credentialDetails.lastUsed)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center space-x-3 p-3 bg-purple-900/20 dark:bg-purple-900/20 rounded-lg">
|
<div className="flex items-center space-x-4 p-4 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
||||||
<Server className="h-5 w-5 text-purple-600 dark:text-purple-400" />
|
<Server className="h-5 w-5 text-zinc-600 dark:text-zinc-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-gray-500">{t('credentials.connectedHosts')}</div>
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('credentials.connectedHosts')}</div>
|
||||||
<div className="font-medium">{hostsUsing.length}</div>
|
<div className="font-medium">{hostsUsing.length}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -343,7 +349,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg flex items-center space-x-2">
|
<CardTitle className="text-lg flex items-center space-x-2">
|
||||||
<Shield className="h-5 w-5 text-green-600" />
|
<Shield className="h-5 w-5 text-zinc-600 dark:text-zinc-400" />
|
||||||
<span>{t('credentials.securityDetails')}</span>
|
<span>{t('credentials.securityDetails')}</span>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
@@ -351,13 +357,13 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-6">
|
<CardContent className="space-y-6">
|
||||||
<div className="flex items-center space-x-3 p-4 bg-green-900/20 dark:bg-green-900/20 rounded-lg">
|
<div className="flex items-center space-x-4 p-6 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
||||||
<CheckCircle className="h-6 w-6 text-green-600" />
|
<CheckCircle className="h-6 w-6 text-zinc-600 dark:text-zinc-400" />
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-green-800 dark:text-green-200">
|
<div className="font-medium text-zinc-800 dark:text-zinc-200">
|
||||||
{t('credentials.credentialSecured')}
|
{t('credentials.credentialSecured')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-green-700 dark:text-green-300">
|
<div className="text-sm text-zinc-700 dark:text-zinc-300">
|
||||||
{t('credentials.credentialSecuredDescription')}
|
{t('credentials.credentialSecuredDescription')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -365,18 +371,18 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
|
|
||||||
{credentialDetails.authType === 'password' && (
|
{credentialDetails.authType === 'password' && (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold mb-3">{t('credentials.passwordAuthentication')}</h3>
|
<h3 className="font-semibold mb-4">{t('credentials.passwordAuthentication')}</h3>
|
||||||
{renderSensitiveField(credentialDetails.password, 'password', t('common.password'))}
|
{renderSensitiveField(credentialDetails.password, 'password', t('common.password'))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{credentialDetails.authType === 'key' && (
|
{credentialDetails.authType === 'key' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
<h3 className="font-semibold">{t('credentials.keyAuthentication')}</h3>
|
<h3 className="font-semibold mb-2">{t('credentials.keyAuthentication')}</h3>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
<div className="text-sm font-medium text-zinc-700 dark:text-zinc-300 mb-3">
|
||||||
{t('credentials.keyType')}
|
{t('credentials.keyType')}
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="text-sm">
|
<Badge variant="outline" className="text-sm">
|
||||||
@@ -395,13 +401,13 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-start space-x-3 p-4 bg-amber-900/20 dark:bg-amber-900/20 rounded-lg">
|
<div className="flex items-start space-x-4 p-6 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
|
||||||
<AlertTriangle className="h-5 w-5 text-amber-600 mt-0.5" />
|
<AlertTriangle className="h-5 w-5 text-zinc-600 dark:text-zinc-400 mt-0.5" />
|
||||||
<div className="text-sm">
|
<div className="text-sm">
|
||||||
<div className="font-medium text-amber-800 dark:text-amber-200 mb-1">
|
<div className="font-medium text-zinc-800 dark:text-zinc-200 mb-2">
|
||||||
{t('credentials.securityReminder')}
|
{t('credentials.securityReminder')}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-amber-700 dark:text-amber-300">
|
<div className="text-zinc-700 dark:text-zinc-300">
|
||||||
{t('credentials.securityReminderText')}
|
{t('credentials.securityReminderText')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -414,15 +420,15 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-lg flex items-center space-x-2">
|
<CardTitle className="text-lg flex items-center space-x-2">
|
||||||
<Server className="h-5 w-5 text-blue-600" />
|
<Server className="h-5 w-5 text-zinc-600 dark:text-zinc-400" />
|
||||||
<span>{t('credentials.hostsUsingCredential')}</span>
|
<span>{t('credentials.hostsUsingCredential')}</span>
|
||||||
<Badge variant="secondary">{hostsUsing.length}</Badge>
|
<Badge variant="secondary">{hostsUsing.length}</Badge>
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{hostsUsing.length === 0 ? (
|
{hostsUsing.length === 0 ? (
|
||||||
<div className="text-center py-8 text-gray-500">
|
<div className="text-center py-10 text-zinc-500 dark:text-zinc-400">
|
||||||
<Server className="h-12 w-12 mx-auto mb-4 text-gray-300" />
|
<Server className="h-12 w-12 mx-auto mb-6 text-zinc-300 dark:text-zinc-600" />
|
||||||
<p>{t('credentials.noHostsUsingCredential')}</p>
|
<p>{t('credentials.noHostsUsingCredential')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -431,22 +437,22 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
|
|||||||
{hostsUsing.map((host) => (
|
{hostsUsing.map((host) => (
|
||||||
<div
|
<div
|
||||||
key={host.id}
|
key={host.id}
|
||||||
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-800 dark:hover:bg-gray-700"
|
className="flex items-center justify-between p-4 border rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800"
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<div className="p-2 bg-blue-100 dark:bg-blue-900 rounded">
|
<div className="p-2 bg-zinc-100 dark:bg-zinc-800 rounded">
|
||||||
<Server className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
<Server className="h-4 w-4 text-zinc-600 dark:text-zinc-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium">
|
<div className="font-medium">
|
||||||
{host.name || `${host.ip}:${host.port}`}
|
{host.name || `${host.ip}:${host.port}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
{host.ip}:{host.port}
|
{host.ip}:{host.port}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-right text-sm text-gray-500">
|
<div className="text-right text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
{formatDate(host.createdAt)}
|
{formatDate(host.createdAt)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -495,7 +495,6 @@ const CredentialsManager: React.FC = () => {
|
|||||||
<Key className="h-16 w-16 mx-auto text-zinc-300 dark:text-zinc-600" />
|
<Key className="h-16 w-16 mx-auto text-zinc-300 dark:text-zinc-600" />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="text-lg font-medium">{t('credentials.noCredentialsYet')}</p>
|
<p className="text-lg font-medium">{t('credentials.noCredentialsYet')}</p>
|
||||||
<p className="text-sm text-zinc-400">开始创建你的第一个SSH凭据</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Button size="lg" onClick={handleCreateCredential}>
|
<Button size="lg" onClick={handleCreateCredential}>
|
||||||
<Plus className="h-5 w-5 mr-2" />
|
<Plus className="h-5 w-5 mr-2" />
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {HostManagerHostViewer} from "@/ui/Desktop/Apps/Host Manager/HostManagerH
|
|||||||
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
import {Tabs, TabsContent, TabsList, TabsTrigger} from "@/components/ui/tabs.tsx";
|
||||||
import {Separator} from "@/components/ui/separator.tsx";
|
import {Separator} from "@/components/ui/separator.tsx";
|
||||||
import {HostManagerHostEditor} from "@/ui/Desktop/Apps/Host Manager/HostManagerHostEditor.tsx";
|
import {HostManagerHostEditor} from "@/ui/Desktop/Apps/Host Manager/HostManagerHostEditor.tsx";
|
||||||
|
import CredentialsManager from "@/ui/Desktop/Apps/Credentials/CredentialsManager.tsx";
|
||||||
import {useSidebar} from "@/components/ui/sidebar.tsx";
|
import {useSidebar} from "@/components/ui/sidebar.tsx";
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): Rea
|
|||||||
<TabsTrigger value="add_host">
|
<TabsTrigger value="add_host">
|
||||||
{editingHost ? t('hosts.editHost') : t('hosts.addHost')}
|
{editingHost ? t('hosts.editHost') : t('hosts.addHost')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="credentials">{t('credentials.credentialsManager')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="host_viewer" className="flex-1 flex flex-col h-full min-h-0">
|
<TabsContent value="host_viewer" className="flex-1 flex flex-col h-full min-h-0">
|
||||||
<Separator className="p-0.25 -mt-0.5 mb-1"/>
|
<Separator className="p-0.25 -mt-0.5 mb-1"/>
|
||||||
@@ -95,6 +97,12 @@ export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): Rea
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="credentials" className="flex-1 flex flex-col h-full min-h-0">
|
||||||
|
<Separator className="p-0.25 -mt-0.5 mb-1"/>
|
||||||
|
<div className="flex flex-col h-full min-h-0 overflow-auto">
|
||||||
|
<CredentialsManager />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
|
|||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts';
|
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts';
|
||||||
import {useTranslation} from "react-i18next";
|
import {useTranslation} from "react-i18next";
|
||||||
|
import {CredentialSelector} from "@/components/CredentialSelector.tsx";
|
||||||
|
|
||||||
interface SSHHost {
|
interface SSHHost {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -44,6 +45,7 @@ interface SSHHost {
|
|||||||
tunnelConnections: any[];
|
tunnelConnections: any[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
credentialId?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SSHManagerHostEditorProps {
|
interface SSHManagerHostEditorProps {
|
||||||
@@ -58,7 +60,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
|
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const [authTab, setAuthTab] = useState<'password' | 'key'>('password');
|
const [authTab, setAuthTab] = useState<'password' | 'key' | 'credential'>('password');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -98,7 +100,8 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
folder: z.string().optional(),
|
folder: z.string().optional(),
|
||||||
tags: z.array(z.string().min(1)).default([]),
|
tags: z.array(z.string().min(1)).default([]),
|
||||||
pin: z.boolean().default(false),
|
pin: z.boolean().default(false),
|
||||||
authType: z.enum(['password', 'key']),
|
authType: z.enum(['password', 'key', 'credential']),
|
||||||
|
credentialId: z.number().optional().nullable(),
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
key: z.instanceof(File).optional().nullable(),
|
key: z.instanceof(File).optional().nullable(),
|
||||||
keyPassword: z.string().optional(),
|
keyPassword: z.string().optional(),
|
||||||
@@ -149,6 +152,14 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
path: ['keyType']
|
path: ['keyType']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
} else if (data.authType === 'credential') {
|
||||||
|
if (!data.credentialId) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: t('hosts.credentialRequired'),
|
||||||
|
path: ['credentialId']
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data.tunnelConnections.forEach((connection, index) => {
|
data.tunnelConnections.forEach((connection, index) => {
|
||||||
@@ -174,7 +185,8 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
folder: editingHost?.folder || "",
|
folder: editingHost?.folder || "",
|
||||||
tags: editingHost?.tags || [],
|
tags: editingHost?.tags || [],
|
||||||
pin: editingHost?.pin || false,
|
pin: editingHost?.pin || false,
|
||||||
authType: (editingHost?.authType as 'password' | 'key') || "password",
|
authType: (editingHost?.authType as 'password' | 'key' | 'credential') || "password",
|
||||||
|
credentialId: editingHost?.credentialId || null,
|
||||||
password: "",
|
password: "",
|
||||||
key: null,
|
key: null,
|
||||||
keyPassword: "",
|
keyPassword: "",
|
||||||
@@ -189,7 +201,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (editingHost) {
|
if (editingHost) {
|
||||||
const defaultAuthType = editingHost.key ? 'key' : 'password';
|
const defaultAuthType = editingHost.credentialId ? 'credential' : (editingHost.key ? 'key' : 'password');
|
||||||
|
|
||||||
setAuthTab(defaultAuthType);
|
setAuthTab(defaultAuthType);
|
||||||
|
|
||||||
@@ -201,7 +213,8 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
folder: editingHost.folder || "",
|
folder: editingHost.folder || "",
|
||||||
tags: editingHost.tags || [],
|
tags: editingHost.tags || [],
|
||||||
pin: editingHost.pin || false,
|
pin: editingHost.pin || false,
|
||||||
authType: defaultAuthType,
|
authType: defaultAuthType as 'password' | 'key' | 'credential',
|
||||||
|
credentialId: editingHost.credentialId || null,
|
||||||
password: editingHost.password || "",
|
password: editingHost.password || "",
|
||||||
key: editingHost.key ? new File([editingHost.key], "key.pem") : null,
|
key: editingHost.key ? new File([editingHost.key], "key.pem") : null,
|
||||||
keyPassword: editingHost.keyPassword || "",
|
keyPassword: editingHost.keyPassword || "",
|
||||||
@@ -224,6 +237,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
tags: [],
|
tags: [],
|
||||||
pin: false,
|
pin: false,
|
||||||
authType: "password",
|
authType: "password",
|
||||||
|
credentialId: null,
|
||||||
password: "",
|
password: "",
|
||||||
key: null,
|
key: null,
|
||||||
keyPassword: "",
|
keyPassword: "",
|
||||||
@@ -574,14 +588,28 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
<Tabs
|
<Tabs
|
||||||
value={authTab}
|
value={authTab}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
setAuthTab(value as 'password' | 'key');
|
setAuthTab(value as 'password' | 'key' | 'credential');
|
||||||
form.setValue('authType', value as 'password' | 'key');
|
form.setValue('authType', value as 'password' | 'key' | 'credential');
|
||||||
|
// Clear other auth fields when switching
|
||||||
|
if (value === 'password') {
|
||||||
|
form.setValue('key', null);
|
||||||
|
form.setValue('keyPassword', '');
|
||||||
|
form.setValue('credentialId', null);
|
||||||
|
} else if (value === 'key') {
|
||||||
|
form.setValue('password', '');
|
||||||
|
form.setValue('credentialId', null);
|
||||||
|
} else if (value === 'credential') {
|
||||||
|
form.setValue('password', '');
|
||||||
|
form.setValue('key', null);
|
||||||
|
form.setValue('keyPassword', '');
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
className="flex-1 flex flex-col h-full min-h-0"
|
className="flex-1 flex flex-col h-full min-h-0"
|
||||||
>
|
>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="password">{t('hosts.password')}</TabsTrigger>
|
<TabsTrigger value="password">{t('hosts.password')}</TabsTrigger>
|
||||||
<TabsTrigger value="key">{t('hosts.key')}</TabsTrigger>
|
<TabsTrigger value="key">{t('hosts.key')}</TabsTrigger>
|
||||||
|
<TabsTrigger value="credential">{t('hosts.credential')}</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<TabsContent value="password">
|
<TabsContent value="password">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -696,6 +724,18 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="credential">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="credentialId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<CredentialSelector
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="terminal">
|
<TabsContent value="terminal">
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ interface SSHHostData {
|
|||||||
folder?: string;
|
folder?: string;
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
pin?: boolean;
|
pin?: boolean;
|
||||||
authType: 'password' | 'key';
|
authType: 'password' | 'key' | 'credential';
|
||||||
password?: string;
|
password?: string;
|
||||||
key?: File | null;
|
key?: File | null;
|
||||||
keyPassword?: string;
|
keyPassword?: string;
|
||||||
keyType?: string;
|
keyType?: string;
|
||||||
|
credentialId?: number | null;
|
||||||
enableTerminal?: boolean;
|
enableTerminal?: boolean;
|
||||||
enableTunnel?: boolean;
|
enableTunnel?: boolean;
|
||||||
enableFileManager?: boolean;
|
enableFileManager?: boolean;
|
||||||
@@ -38,6 +39,7 @@ interface SSHHost {
|
|||||||
key?: string;
|
key?: string;
|
||||||
keyPassword?: string;
|
keyPassword?: string;
|
||||||
keyType?: string;
|
keyType?: string;
|
||||||
|
credentialId?: number;
|
||||||
enableTerminal: boolean;
|
enableTerminal: boolean;
|
||||||
enableTunnel: boolean;
|
enableTunnel: boolean;
|
||||||
enableFileManager: boolean;
|
enableFileManager: boolean;
|
||||||
@@ -342,6 +344,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
|
|||||||
key: hostData.authType === 'key' ? hostData.key : null,
|
key: hostData.authType === 'key' ? hostData.key : null,
|
||||||
keyPassword: hostData.authType === 'key' ? hostData.keyPassword : '',
|
keyPassword: hostData.authType === 'key' ? hostData.keyPassword : '',
|
||||||
keyType: hostData.authType === 'key' ? hostData.keyType : '',
|
keyType: hostData.authType === 'key' ? hostData.keyType : '',
|
||||||
|
credentialId: hostData.authType === 'credential' ? hostData.credentialId : null,
|
||||||
enableTerminal: hostData.enableTerminal !== false,
|
enableTerminal: hostData.enableTerminal !== false,
|
||||||
enableTunnel: hostData.enableTunnel !== false,
|
enableTunnel: hostData.enableTunnel !== false,
|
||||||
enableFileManager: hostData.enableFileManager !== false,
|
enableFileManager: hostData.enableFileManager !== false,
|
||||||
@@ -393,6 +396,7 @@ export async function updateSSHHost(hostId: number, hostData: SSHHostData): Prom
|
|||||||
key: hostData.authType === 'key' ? hostData.key : null,
|
key: hostData.authType === 'key' ? hostData.key : null,
|
||||||
keyPassword: hostData.authType === 'key' ? hostData.keyPassword : '',
|
keyPassword: hostData.authType === 'key' ? hostData.keyPassword : '',
|
||||||
keyType: hostData.authType === 'key' ? hostData.keyType : '',
|
keyType: hostData.authType === 'key' ? hostData.keyType : '',
|
||||||
|
credentialId: hostData.authType === 'credential' ? hostData.credentialId : null,
|
||||||
enableTerminal: hostData.enableTerminal !== false,
|
enableTerminal: hostData.enableTerminal !== false,
|
||||||
enableTunnel: hostData.enableTunnel !== false,
|
enableTunnel: hostData.enableTunnel !== false,
|
||||||
enableFileManager: hostData.enableFileManager !== false,
|
enableFileManager: hostData.enableFileManager !== false,
|
||||||
@@ -817,8 +821,9 @@ export async function getOIDCConfig(): Promise<any> {
|
|||||||
try {
|
try {
|
||||||
const response = await authApi.get('/users/oidc-config');
|
const response = await authApi.get('/users/oidc-config');
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
handleApiError(error, 'fetch OIDC config');
|
console.warn('Failed to fetch OIDC config:', error.response?.data?.error || error.message);
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user