Integrate credentials manager with host authentication system

- Add credential selection tab to Host Manager editor
- Create CredentialSelector component for credential selection
- Update form validation to support credential-based authentication
- Extend SSH host interfaces with credentialId field
- Add internationalization support for credential authentication
- Fix OIDC config endpoint to return 200 with null data instead of 404
- Improve credentials UI spacing and color consistency
- Remove hardcoded colors and use Zinc theme throughout
This commit is contained in:
ZacharyZcR
2025-09-06 17:44:41 +08:00
parent 81fca5b074
commit 199a9f6e52
14 changed files with 429 additions and 158 deletions
+6 -1
View File
@@ -12,7 +12,12 @@
"Bash(npm run build:*)",
"Bash(npm install)",
"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": [],
"ask": []
+2 -1
View File
@@ -21,7 +21,8 @@ function startBackendServer() {
backendProcess = spawn('node', [backendPath], {
stdio: ['ignore', 'pipe', 'pipe'],
detached: false
detached: false,
cwd: path.join(__dirname, '..') // Set working directory to app root
});
backendProcess.stdout.on('data', (data) => {
+5
View File
@@ -131,6 +131,7 @@
"loading": "Loading",
"required": "Required",
"optional": "Optional",
"clear": "Clear",
"toggleSidebar": "Toggle Sidebar",
"sidebar": "Sidebar",
"home": "Home",
@@ -376,6 +377,10 @@
"authentication": "Authentication",
"password": "Password",
"key": "Key",
"credential": "Credential",
"selectCredential": "Select Credential",
"selectCredentialPlaceholder": "Choose a credential...",
"credentialRequired": "Credential is required when using credential authentication",
"sshPrivateKey": "SSH Private Key",
"keyPassword": "Key Password",
"keyType": "Key Type",
+5
View File
@@ -131,6 +131,7 @@
"loading": "加载中",
"required": "必填",
"optional": "可选",
"clear": "清除",
"toggleSidebar": "切换侧边栏",
"sidebar": "侧边栏",
"home": "首页",
@@ -397,6 +398,10 @@
"authentication": "认证方式",
"password": "密码",
"key": "密钥",
"credential": "凭证",
"selectCredential": "选择凭证",
"selectCredentialPlaceholder": "选择一个凭证...",
"credentialRequired": "使用凭证认证时需要选择凭证",
"sshPrivateKey": "SSH 私钥",
"keyPassword": "密钥密码",
"keyType": "密钥类型",
+1 -1
View File
@@ -274,7 +274,7 @@ router.get('/oidc-config', async (req, res) => {
try {
const row = db.$client.prepare("SELECT value FROM settings WHERE key = 'oidc_config'").get();
if (!row) {
return res.status(404).json({error: 'OIDC not configured'});
return res.json(null);
}
res.json(JSON.parse((row as any).value));
} catch (err) {
+200
View File
@@ -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>
);
}
+2 -2
View File
@@ -58,9 +58,9 @@ function SheetContent({
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",
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" &&
"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" &&
"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" &&
+5 -3
View File
@@ -79,7 +79,9 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
.then(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();
}, []);
@@ -370,7 +372,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
<TableCell className="px-4">
<Button variant="ghost" size="sm"
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}>
<Trash2 className="h-4 w-4"/>
</Button>
@@ -434,7 +436,7 @@ export function AdminSettings({isTopbarOpen = true}: AdminSettingsProps): React.
<TableCell className="px-4">
<Button variant="ghost" size="sm"
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"/>
Remove Admin
</Button>
@@ -211,15 +211,15 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
return (
<Sheet open={true} onOpenChange={onCancel}>
<SheetContent className="w-[800px] max-w-[90vw] overflow-y-auto">
<SheetHeader>
<SheetTitle className="flex items-center space-x-2">
<Key className="h-5 w-5 text-blue-600" />
<span>
<SheetContent className="w-[600px] max-w-[50vw] overflow-y-auto">
<SheetHeader className="space-y-4 pb-8">
<SheetTitle className="flex items-center space-x-3">
<Key className="h-6 w-6 text-zinc-600 dark:text-zinc-400" />
<span className="text-xl font-semibold">
{credential ? t('credentials.editCredential') : t('credentials.createCredential')}
</span>
</SheetTitle>
<SheetDescription>
<SheetDescription className="text-base text-zinc-600 dark:text-zinc-400">
{credential
? t('credentials.editCredentialDescription')
: t('credentials.createCredentialDescription')
@@ -227,26 +227,26 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
</SheetDescription>
</SheetHeader>
<form onSubmit={handleSubmit} className="space-y-6">
<form onSubmit={handleSubmit} className="space-y-10 px-2">
<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="auth">{t('credentials.authentication')}</TabsTrigger>
<TabsTrigger value="organization">{t('credentials.organization')}</TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4">
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('credentials.basicInformation')}</CardTitle>
<CardDescription>
<TabsContent value="basic" className="space-y-8 mt-8">
<Card className="border-zinc-200 dark:border-zinc-700">
<CardHeader className="pb-8">
<CardTitle className="text-lg font-semibold">{t('credentials.basicInformation')}</CardTitle>
<CardDescription className="text-zinc-600 dark:text-zinc-400">
{t('credentials.basicInformationDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="name" className="flex items-center space-x-1">
<User className="h-4 w-4" />
<CardContent className="space-y-8">
<div className="space-y-4">
<Label htmlFor="name" className="flex items-center space-x-2 text-sm font-medium">
<User className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
<span>{t('credentials.credentialName')}</span>
<span className="text-red-500">*</span>
</Label>
@@ -265,7 +265,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
)}
</div>
<div className="space-y-2">
<div className="space-y-4">
<Label htmlFor="description">{t('credentials.credentialDescription')}</Label>
<Textarea
id="description"
@@ -276,7 +276,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
/>
</div>
<div className="space-y-2">
<div className="space-y-4">
<Label htmlFor="username" className="flex items-center space-x-1">
<User className="h-4 w-4" />
<span>{t('common.username')}</span>
@@ -300,56 +300,56 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
</Card>
</TabsContent>
<TabsContent value="auth" className="space-y-4">
<TabsContent value="auth" className="space-y-6 mt-8">
<Card>
<CardHeader>
<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>
</CardTitle>
<CardDescription>
{t('credentials.authenticationMethodDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-3">
<CardContent className="space-y-6">
<div className="space-y-4">
<Label>{t('credentials.authenticationType')}</Label>
<div className="flex space-x-4">
<div className="flex space-x-6">
<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'
? 'border-blue-500 bg-blue-900/20 dark:bg-blue-900/20'
: 'border-gray-600 hover:border-gray-500 dark:border-gray-600 dark:hover:border-gray-500'
? 'border-zinc-500 bg-zinc-900/20 dark:bg-zinc-900/20'
: 'border-zinc-600 hover:border-zinc-500 dark:border-zinc-600 dark:hover:border-zinc-500'
}`}
onClick={() => setFormData(prev => ({ ...prev, authType: 'password' }))}
>
<div className="flex items-center space-x-3">
<Lock className="h-5 w-5 text-orange-500" />
<div className="flex items-center space-x-4">
<Lock className="h-5 w-5 text-zinc-600 dark:text-zinc-400" />
<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>
{formData.authType === 'password' && (
<Check className="h-5 w-5 text-blue-500" />
<Check className="h-5 w-5 text-zinc-500" />
)}
</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'
? 'border-green-500 bg-green-900/20 dark:bg-green-900/20'
: 'border-gray-600 hover:border-gray-500 dark:border-gray-600 dark:hover:border-gray-500'
? 'border-zinc-500 bg-zinc-900/20 dark:bg-zinc-900/20'
: 'border-zinc-600 hover:border-zinc-500 dark:border-zinc-600 dark:hover:border-zinc-500'
}`}
onClick={() => setFormData(prev => ({ ...prev, authType: 'key' }))}
>
<div className="flex items-center space-x-3">
<Key className="h-5 w-5 text-green-500" />
<div className="flex items-center space-x-4">
<Key className="h-5 w-5 text-zinc-500 dark:text-zinc-400" />
<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>
{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>
@@ -359,7 +359,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
<Separator />
{formData.authType === 'password' && (
<div className="space-y-2">
<div className="space-y-4">
<Label htmlFor="password" className="flex items-center space-x-1">
<Lock className="h-4 w-4" />
<span>{t('common.password')}</span>
@@ -394,8 +394,8 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
)}
{formData.authType === 'key' && (
<div className="space-y-6">
<div className="space-y-4">
<div className="space-y-2">
<Label className="flex items-center space-x-1">
<Key className="h-4 w-4" />
<span>{t('credentials.sshKeyType')}</span>
@@ -412,7 +412,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
</Select>
</div>
<div className="space-y-2">
<div className="space-y-4">
<Label htmlFor="key" className="flex items-center space-x-1">
<Key className="h-4 w-4" />
<span>{t('credentials.privateKey')}</span>
@@ -432,7 +432,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
<span>{errors.key}</span>
</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()}>
<Upload className="h-4 w-4 mr-1" />
{t('credentials.uploadKeyFile')}
@@ -451,7 +451,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
/>
</div>
<div className="space-y-2">
<div className="space-y-4">
<Label htmlFor="keyPassword">{t('credentials.keyPassphrase')}</Label>
<div className="relative">
<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" />}
</Button>
</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>
)}
@@ -480,29 +480,29 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
</Card>
</TabsContent>
<TabsContent value="organization" className="space-y-4">
<TabsContent value="organization" className="space-y-6 mt-8">
<Card>
<CardHeader>
<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>
</CardTitle>
<CardDescription>
{t('credentials.organizationDescription')}
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<CardContent className="space-y-6">
<div className="space-y-4">
<Label htmlFor="folder" className="flex items-center space-x-1">
<Folder className="h-4 w-4" />
<span>{t('common.folder')}</span>
</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>
<SelectValue placeholder={t('credentials.selectOrCreateFolder')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="">{t('credentials.noFolder')}</SelectItem>
<SelectItem value="__none__">{t('credentials.noFolder')}</SelectItem>
{existingFolders.map(folder => (
<SelectItem key={folder} value={folder}>{folder}</SelectItem>
))}
@@ -515,12 +515,12 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
/>
</div>
<div className="space-y-2">
<div className="space-y-4">
<Label className="flex items-center space-x-1">
<Tag className="h-4 w-4" />
<span>{t('hosts.tags')}</span>
</Label>
<div className="flex flex-wrap gap-2 mb-2">
<div className="flex flex-wrap gap-3 mb-4">
{formData.tags.map((tag, index) => (
<Badge key={index} variant="secondary" className="pr-1">
{tag}
@@ -536,7 +536,7 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
</Badge>
))}
</div>
<div className="flex space-x-2">
<div className="flex space-x-3">
<Input
placeholder={t('credentials.addTag')}
value={newTag}
@@ -558,17 +558,12 @@ const CredentialEditor: React.FC<CredentialEditorProps> = ({ credential, onSave,
</TabsContent>
</Tabs>
<SheetFooter className="flex justify-between">
<div className="flex space-x-2">
<Button type="button" variant="outline" onClick={testConnection}>
{t('credentials.testConnection')}
</Button>
</div>
<div className="flex space-x-2">
<Button type="button" variant="outline" onClick={onCancel}>
<SheetFooter className="flex justify-end items-center pt-8 border-t border-zinc-200 dark:border-zinc-700">
<div className="flex space-x-4">
<Button type="button" variant="outline" size="lg" onClick={onCancel} className="border-zinc-300 dark:border-zinc-600">
{t('common.cancel')}
</Button>
<Button type="submit" disabled={saving}>
<Button type="submit" size="lg" disabled={saving}>
{saving ? t('credentials.saving') : credential ? t('credentials.updateCredential') : t('credentials.createCredential')}
</Button>
</div>
@@ -120,9 +120,9 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
const getAuthIcon = (authType: string) => {
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 (
<div className="space-y-2">
<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>
<div className="flex items-center space-x-2">
@@ -159,13 +159,13 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
</Button>
</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 ? (
<pre className={`text-sm ${isMultiline ? 'whitespace-pre-wrap' : 'whitespace-nowrap'} font-mono`}>
{value}
</pre>
) : (
<div className="text-sm text-gray-500">
<div className="text-sm text-zinc-500 dark:text-zinc-400">
{'•'.repeat(isMultiline ? 50 : 20)}
</div>
)}
@@ -177,9 +177,9 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
if (loading || !credentialDetails) {
return (
<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="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>
</SheetContent>
</Sheet>
@@ -188,35 +188,39 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
return (
<Sheet open={true} onOpenChange={onClose}>
<SheetContent className="w-[800px] max-w-[90vw] overflow-y-auto">
<SheetHeader>
<SheetTitle className="flex items-center space-x-3">
<SheetContent className="w-[600px] max-w-[50vw] overflow-y-auto">
<SheetHeader className="space-y-6 pb-8">
<SheetTitle className="flex items-center space-x-4">
<div className="p-2 rounded-lg bg-zinc-100 dark:bg-zinc-800">
{getAuthIcon(credentialDetails.authType)}
<div>
<div>{credentialDetails.name}</div>
<div className="text-sm font-normal text-gray-600 dark:text-gray-400">
</div>
<div className="flex-1">
<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}
</div>
</div>
<div className="flex items-center space-x-2 ml-auto">
<Badge variant={credentialDetails.authType === 'password' ? 'secondary' : 'outline'}>
<div className="flex items-center space-x-2">
<Badge variant="outline" className="border-zinc-300 dark:border-zinc-600 text-zinc-600 dark:text-zinc-400">
{credentialDetails.authType}
</Badge>
{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>
</SheetTitle>
</SheetHeader>
<div className="space-y-6">
<div className="space-y-10">
{/* 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
variant={activeTab === 'overview' ? 'default' : 'ghost'}
size="sm"
onClick={() => setActiveTab('overview')}
className="flex-1"
className="flex-1 h-10"
>
<FileText className="h-4 w-4 mr-2" />
{t('credentials.overview')}
@@ -225,7 +229,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
variant={activeTab === 'security' ? 'default' : 'ghost'}
size="sm"
onClick={() => setActiveTab('security')}
className="flex-1"
className="flex-1 h-10"
>
<Shield className="h-4 w-4 mr-2" />
{t('credentials.security')}
@@ -234,7 +238,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
variant={activeTab === 'usage' ? 'default' : 'ghost'}
size="sm"
onClick={() => setActiveTab('usage')}
className="flex-1"
className="flex-1 h-10"
>
<Server className="h-4 w-4 mr-2" />
{t('credentials.usage')}
@@ -243,36 +247,38 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
{/* Tab Content */}
{activeTab === 'overview' && (
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-lg">{t('credentials.basicInformation')}</CardTitle>
<div className="grid gap-10 lg:grid-cols-2">
<Card className="border-zinc-200 dark:border-zinc-700">
<CardHeader className="pb-8">
<CardTitle className="text-lg font-semibold">{t('credentials.basicInformation')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center space-x-3">
<User className="h-4 w-4 text-gray-500" />
<CardContent className="space-y-8">
<div className="flex items-center space-x-5">
<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 className="text-sm text-gray-500">{t('common.username')}</div>
<div className="font-medium">{credentialDetails.username}</div>
<div className="text-sm text-zinc-500 dark:text-zinc-400">{t('common.username')}</div>
<div className="font-medium text-zinc-800 dark:text-zinc-200">{credentialDetails.username}</div>
</div>
</div>
{credentialDetails.folder && (
<div className="flex items-center space-x-3">
<Folder className="h-4 w-4 text-gray-500" />
<div className="flex items-center space-x-4">
<Folder className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
<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>
</div>
)}
{credentialDetails.tags.length > 0 && (
<div className="flex items-start space-x-3">
<Hash className="h-4 w-4 text-gray-500 mt-1" />
<div className="flex items-start space-x-4">
<Hash className="h-4 w-4 text-zinc-500 dark:text-zinc-400 mt-1" />
<div className="flex-1">
<div className="text-sm text-gray-500 mb-2">{t('hosts.tags')}</div>
<div className="flex flex-wrap gap-1">
<div className="text-sm text-zinc-500 dark:text-zinc-400 mb-3">{t('hosts.tags')}</div>
<div className="flex flex-wrap gap-2">
{credentialDetails.tags.map((tag, index) => (
<Badge key={index} variant="outline" className="text-xs">
{tag}
@@ -285,18 +291,18 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
<Separator />
<div className="flex items-center space-x-3">
<Calendar className="h-4 w-4 text-gray-500" />
<div className="flex items-center space-x-4">
<Calendar className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
<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>
</div>
<div className="flex items-center space-x-3">
<Calendar className="h-4 w-4 text-gray-500" />
<div className="flex items-center space-x-4">
<Calendar className="h-4 w-4 text-zinc-500 dark:text-zinc-400" />
<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>
</div>
@@ -307,30 +313,30 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
<CardHeader>
<CardTitle className="text-lg">{t('credentials.usageStatistics')}</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-center p-4 bg-blue-900/20 dark:bg-blue-900/20 rounded-lg">
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
<CardContent className="space-y-6">
<div className="text-center p-6 bg-zinc-900/20 dark:bg-zinc-900/20 rounded-lg">
<div className="text-3xl font-bold text-zinc-600 dark:text-zinc-400">
{credentialDetails.usageCount}
</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')}
</div>
</div>
{credentialDetails.lastUsed && (
<div className="flex items-center space-x-3 p-3 bg-green-900/20 dark:bg-green-900/20 rounded-lg">
<Clock className="h-5 w-5 text-green-600 dark:text-green-400" />
<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-zinc-600 dark:text-zinc-400" />
<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>
</div>
)}
<div className="flex items-center space-x-3 p-3 bg-purple-900/20 dark:bg-purple-900/20 rounded-lg">
<Server className="h-5 w-5 text-purple-600 dark:text-purple-400" />
<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-zinc-600 dark:text-zinc-400" />
<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>
</div>
@@ -343,7 +349,7 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
<Card>
<CardHeader>
<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>
</CardTitle>
<CardDescription>
@@ -351,13 +357,13 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
</CardDescription>
</CardHeader>
<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">
<CheckCircle className="h-6 w-6 text-green-600" />
<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-zinc-600 dark:text-zinc-400" />
<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')}
</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')}
</div>
</div>
@@ -365,18 +371,18 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
{credentialDetails.authType === 'password' && (
<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'))}
</div>
)}
{credentialDetails.authType === 'key' && (
<div className="space-y-4">
<h3 className="font-semibold">{t('credentials.keyAuthentication')}</h3>
<div className="space-y-6">
<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 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')}
</div>
<Badge variant="outline" className="text-sm">
@@ -395,13 +401,13 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
</div>
)}
<div className="flex items-start space-x-3 p-4 bg-amber-900/20 dark:bg-amber-900/20 rounded-lg">
<AlertTriangle className="h-5 w-5 text-amber-600 mt-0.5" />
<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-zinc-600 dark:text-zinc-400 mt-0.5" />
<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')}
</div>
<div className="text-amber-700 dark:text-amber-300">
<div className="text-zinc-700 dark:text-zinc-300">
{t('credentials.securityReminderText')}
</div>
</div>
@@ -414,15 +420,15 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
<Card>
<CardHeader>
<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>
<Badge variant="secondary">{hostsUsing.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
{hostsUsing.length === 0 ? (
<div className="text-center py-8 text-gray-500">
<Server className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<div className="text-center py-10 text-zinc-500 dark:text-zinc-400">
<Server className="h-12 w-12 mx-auto mb-6 text-zinc-300 dark:text-zinc-600" />
<p>{t('credentials.noHostsUsingCredential')}</p>
</div>
) : (
@@ -431,22 +437,22 @@ const CredentialViewer: React.FC<CredentialViewerProps> = ({ credential, onClose
{hostsUsing.map((host) => (
<div
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="p-2 bg-blue-100 dark:bg-blue-900 rounded">
<Server className="h-4 w-4 text-blue-600 dark:text-blue-400" />
<div className="p-2 bg-zinc-100 dark:bg-zinc-800 rounded">
<Server className="h-4 w-4 text-zinc-600 dark:text-zinc-400" />
</div>
<div>
<div className="font-medium">
{host.name || `${host.ip}:${host.port}`}
</div>
<div className="text-sm text-gray-500">
<div className="text-sm text-zinc-500 dark:text-zinc-400">
{host.ip}:{host.port}
</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)}
</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" />
<div className="space-y-2">
<p className="text-lg font-medium">{t('credentials.noCredentialsYet')}</p>
<p className="text-sm text-zinc-400">SSH凭据</p>
</div>
<Button size="lg" onClick={handleCreateCredential}>
<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 {Separator} from "@/components/ui/separator.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 {useTranslation} from "react-i18next";
@@ -81,6 +82,7 @@ export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): Rea
<TabsTrigger value="add_host">
{editingHost ? t('hosts.editHost') : t('hosts.addHost')}
</TabsTrigger>
<TabsTrigger value="credentials">{t('credentials.credentialsManager')}</TabsTrigger>
</TabsList>
<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"/>
@@ -95,6 +97,12 @@ export function HostManager({onSelectView, isTopbarOpen}: HostManagerProps): Rea
/>
</div>
</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>
</div>
</div>
@@ -22,6 +22,7 @@ import {Alert, AlertDescription} from "@/components/ui/alert.tsx";
import {toast} from "sonner";
import {createSSHHost, updateSSHHost, getSSHHosts} from '@/ui/main-axios.ts';
import {useTranslation} from "react-i18next";
import {CredentialSelector} from "@/components/CredentialSelector.tsx";
interface SSHHost {
id: number;
@@ -44,6 +45,7 @@ interface SSHHost {
tunnelConnections: any[];
createdAt: string;
updatedAt: string;
credentialId?: number;
}
interface SSHManagerHostEditorProps {
@@ -58,7 +60,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
const [sshConfigurations, setSshConfigurations] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
const [authTab, setAuthTab] = useState<'password' | 'key'>('password');
const [authTab, setAuthTab] = useState<'password' | 'key' | 'credential'>('password');
useEffect(() => {
const fetchData = async () => {
@@ -98,7 +100,8 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
folder: z.string().optional(),
tags: z.array(z.string().min(1)).default([]),
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(),
key: z.instanceof(File).optional().nullable(),
keyPassword: z.string().optional(),
@@ -149,6 +152,14 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
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) => {
@@ -174,7 +185,8 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
folder: editingHost?.folder || "",
tags: editingHost?.tags || [],
pin: editingHost?.pin || false,
authType: (editingHost?.authType as 'password' | 'key') || "password",
authType: (editingHost?.authType as 'password' | 'key' | 'credential') || "password",
credentialId: editingHost?.credentialId || null,
password: "",
key: null,
keyPassword: "",
@@ -189,7 +201,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
useEffect(() => {
if (editingHost) {
const defaultAuthType = editingHost.key ? 'key' : 'password';
const defaultAuthType = editingHost.credentialId ? 'credential' : (editingHost.key ? 'key' : 'password');
setAuthTab(defaultAuthType);
@@ -201,7 +213,8 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
folder: editingHost.folder || "",
tags: editingHost.tags || [],
pin: editingHost.pin || false,
authType: defaultAuthType,
authType: defaultAuthType as 'password' | 'key' | 'credential',
credentialId: editingHost.credentialId || null,
password: editingHost.password || "",
key: editingHost.key ? new File([editingHost.key], "key.pem") : null,
keyPassword: editingHost.keyPassword || "",
@@ -224,6 +237,7 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
tags: [],
pin: false,
authType: "password",
credentialId: null,
password: "",
key: null,
keyPassword: "",
@@ -574,14 +588,28 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
<Tabs
value={authTab}
onValueChange={(value) => {
setAuthTab(value as 'password' | 'key');
form.setValue('authType', value as 'password' | 'key');
setAuthTab(value as 'password' | 'key' | 'credential');
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"
>
<TabsList>
<TabsTrigger value="password">{t('hosts.password')}</TabsTrigger>
<TabsTrigger value="key">{t('hosts.key')}</TabsTrigger>
<TabsTrigger value="credential">{t('hosts.credential')}</TabsTrigger>
</TabsList>
<TabsContent value="password">
<FormField
@@ -696,6 +724,18 @@ export function HostManagerHostEditor({editingHost, onFormSubmit}: SSHManagerHos
/>
</div>
</TabsContent>
<TabsContent value="credential">
<FormField
control={form.control}
name="credentialId"
render={({ field }) => (
<CredentialSelector
value={field.value}
onValueChange={field.onChange}
/>
)}
/>
</TabsContent>
</Tabs>
</TabsContent>
<TabsContent value="terminal">
+8 -3
View File
@@ -12,11 +12,12 @@ interface SSHHostData {
folder?: string;
tags?: string[];
pin?: boolean;
authType: 'password' | 'key';
authType: 'password' | 'key' | 'credential';
password?: string;
key?: File | null;
keyPassword?: string;
keyType?: string;
credentialId?: number | null;
enableTerminal?: boolean;
enableTunnel?: boolean;
enableFileManager?: boolean;
@@ -38,6 +39,7 @@ interface SSHHost {
key?: string;
keyPassword?: string;
keyType?: string;
credentialId?: number;
enableTerminal: boolean;
enableTunnel: boolean;
enableFileManager: boolean;
@@ -342,6 +344,7 @@ export async function createSSHHost(hostData: SSHHostData): Promise<SSHHost> {
key: hostData.authType === 'key' ? hostData.key : null,
keyPassword: hostData.authType === 'key' ? hostData.keyPassword : '',
keyType: hostData.authType === 'key' ? hostData.keyType : '',
credentialId: hostData.authType === 'credential' ? hostData.credentialId : null,
enableTerminal: hostData.enableTerminal !== false,
enableTunnel: hostData.enableTunnel !== 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,
keyPassword: hostData.authType === 'key' ? hostData.keyPassword : '',
keyType: hostData.authType === 'key' ? hostData.keyType : '',
credentialId: hostData.authType === 'credential' ? hostData.credentialId : null,
enableTerminal: hostData.enableTerminal !== false,
enableTunnel: hostData.enableTunnel !== false,
enableFileManager: hostData.enableFileManager !== false,
@@ -817,8 +821,9 @@ export async function getOIDCConfig(): Promise<any> {
try {
const response = await authApi.get('/users/oidc-config');
return response.data;
} catch (error) {
handleApiError(error, 'fetch OIDC config');
} catch (error: any) {
console.warn('Failed to fetch OIDC config:', error.response?.data?.error || error.message);
return null;
}
}