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
@@ -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">