Add password-bootstrap SSH setup for new connections
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import client from './client'
|
||||
|
||||
export type AuthType = 'PASSWORD' | 'PRIVATE_KEY'
|
||||
export type AuthType = 'PASSWORD' | 'PRIVATE_KEY'
|
||||
export type ConnectionSetupMode = 'NONE' | 'PASSWORD_BOOTSTRAP'
|
||||
|
||||
export interface Connection {
|
||||
id: number
|
||||
@@ -18,11 +19,13 @@ export interface ConnectionCreateRequest {
|
||||
host: string
|
||||
port?: number
|
||||
username: string
|
||||
authType?: AuthType
|
||||
password?: string
|
||||
privateKey?: string
|
||||
passphrase?: string
|
||||
}
|
||||
authType?: AuthType
|
||||
password?: string
|
||||
privateKey?: string
|
||||
passphrase?: string
|
||||
setupMode?: ConnectionSetupMode
|
||||
bootstrapPassword?: string
|
||||
}
|
||||
|
||||
export function listConnections() {
|
||||
return client.get<Connection[]>('/connections')
|
||||
|
||||
@@ -16,15 +16,18 @@ const emit = defineEmits<{
|
||||
const name = ref('')
|
||||
const host = ref('')
|
||||
const port = ref(22)
|
||||
const username = ref('')
|
||||
const authType = ref<AuthType>('PASSWORD')
|
||||
const password = ref('')
|
||||
const privateKey = ref('')
|
||||
const privateKeyFileName = ref('')
|
||||
const privateKeyInputRef = ref<HTMLInputElement | null>(null)
|
||||
const passphrase = ref('')
|
||||
|
||||
const isEdit = computed(() => !!props.connection)
|
||||
const username = ref('')
|
||||
const authType = ref<AuthType>('PASSWORD')
|
||||
const password = ref('')
|
||||
const privateKey = ref('')
|
||||
const privateKeyFileName = ref('')
|
||||
const privateKeyInputRef = ref<HTMLInputElement | null>(null)
|
||||
const passphrase = ref('')
|
||||
const passwordBootstrapEnabled = ref(false)
|
||||
const bootstrapPassword = ref('')
|
||||
|
||||
const isEdit = computed(() => !!props.connection)
|
||||
const isPasswordBootstrapMode = computed(() => !isEdit.value && passwordBootstrapEnabled.value)
|
||||
|
||||
const hostError = computed(() => {
|
||||
const h = host.value.trim()
|
||||
@@ -54,21 +57,25 @@ watch(
|
||||
host.value = c.host
|
||||
port.value = c.port
|
||||
username.value = c.username
|
||||
authType.value = c.authType
|
||||
authType.value = c.authType
|
||||
password.value = ''
|
||||
privateKey.value = ''
|
||||
privateKeyFileName.value = ''
|
||||
passphrase.value = ''
|
||||
passwordBootstrapEnabled.value = false
|
||||
bootstrapPassword.value = ''
|
||||
} else {
|
||||
name.value = ''
|
||||
host.value = ''
|
||||
port.value = 22
|
||||
username.value = ''
|
||||
authType.value = 'PASSWORD'
|
||||
name.value = ''
|
||||
host.value = ''
|
||||
port.value = 22
|
||||
username.value = ''
|
||||
authType.value = 'PASSWORD'
|
||||
password.value = ''
|
||||
privateKey.value = ''
|
||||
privateKeyFileName.value = ''
|
||||
passphrase.value = ''
|
||||
passwordBootstrapEnabled.value = false
|
||||
bootstrapPassword.value = ''
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -147,36 +154,45 @@ async function handleSubmit() {
|
||||
error.value = '请填写主机'
|
||||
return
|
||||
}
|
||||
if (!username.value.trim()) {
|
||||
error.value = '请填写用户名'
|
||||
return
|
||||
}
|
||||
if (authType.value === 'PASSWORD' && !isEdit.value && !password.value) {
|
||||
error.value = '请填写密码'
|
||||
return
|
||||
}
|
||||
if (authType.value === 'PRIVATE_KEY' && !isEdit.value && !privateKey.value.trim()) {
|
||||
error.value = '请填写私钥'
|
||||
return
|
||||
}
|
||||
if (!username.value.trim()) {
|
||||
error.value = '请填写用户名'
|
||||
return
|
||||
}
|
||||
if (isPasswordBootstrapMode.value && !bootstrapPassword.value) {
|
||||
error.value = '请填写初始登录密码'
|
||||
return
|
||||
}
|
||||
if (!isPasswordBootstrapMode.value && authType.value === 'PASSWORD' && !isEdit.value && !password.value) {
|
||||
error.value = '请填写密码'
|
||||
return
|
||||
}
|
||||
if (!isPasswordBootstrapMode.value && authType.value === 'PRIVATE_KEY' && !isEdit.value && !privateKey.value.trim()) {
|
||||
error.value = '请填写私钥'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const data: ConnectionCreateRequest = {
|
||||
name: name.value.trim(),
|
||||
host: host.value.trim(),
|
||||
port: port.value,
|
||||
username: username.value.trim(),
|
||||
authType: authType.value,
|
||||
}
|
||||
if (authType.value === 'PASSWORD' && password.value) {
|
||||
data.password = password.value
|
||||
}
|
||||
if (authType.value === 'PRIVATE_KEY') {
|
||||
if (privateKey.value.trim()) data.privateKey = privateKey.value.trim()
|
||||
if (passphrase.value) data.passphrase = passphrase.value
|
||||
}
|
||||
try {
|
||||
const data: ConnectionCreateRequest = {
|
||||
name: name.value.trim(),
|
||||
host: host.value.trim(),
|
||||
port: port.value,
|
||||
username: username.value.trim(),
|
||||
}
|
||||
if (isPasswordBootstrapMode.value) {
|
||||
data.setupMode = 'PASSWORD_BOOTSTRAP'
|
||||
data.bootstrapPassword = bootstrapPassword.value
|
||||
} else {
|
||||
data.authType = authType.value
|
||||
if (authType.value === 'PASSWORD' && password.value) {
|
||||
data.password = password.value
|
||||
}
|
||||
if (authType.value === 'PRIVATE_KEY') {
|
||||
if (privateKey.value.trim()) data.privateKey = privateKey.value.trim()
|
||||
if (passphrase.value) data.passphrase = passphrase.value
|
||||
}
|
||||
}
|
||||
try {
|
||||
if (props.onSave) {
|
||||
await props.onSave(data)
|
||||
} else {
|
||||
@@ -252,42 +268,71 @@ async function handleSubmit() {
|
||||
<p v-if="portError" class="mt-1 text-xs text-red-400">{{ portError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-slate-300 mb-1">用户名</label>
|
||||
<input
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium text-slate-300 mb-1">用户名</label>
|
||||
<input
|
||||
id="username"
|
||||
v-model="username"
|
||||
type="text"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-slate-700 border border-slate-600 text-slate-100 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
placeholder="root"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">认证方式</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
placeholder="root"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isEdit" class="rounded-lg border border-cyan-900/70 bg-cyan-950/40 p-4 space-y-3">
|
||||
<label class="flex items-start gap-3 cursor-pointer">
|
||||
<input v-model="passwordBootstrapEnabled" type="checkbox" class="mt-1 rounded border-slate-500 bg-slate-700 text-cyan-500" />
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-100">一键免密配置</p>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-400">
|
||||
使用一次初始密码为远端追加公钥,创建成功后会保存为系统内的私钥连接。
|
||||
不会修改你本机的 <code>~/.ssh/config</code>。
|
||||
</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="!isPasswordBootstrapMode">
|
||||
<label class="block text-sm font-medium text-slate-300 mb-2">认证方式</label>
|
||||
<div class="flex gap-4">
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input v-model="authType" type="radio" value="PASSWORD" class="rounded" />
|
||||
<span class="text-slate-300">密码</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input v-model="authType" type="radio" value="PRIVATE_KEY" class="rounded" />
|
||||
<span class="text-slate-300">私钥</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="authType === 'PASSWORD'">
|
||||
<label for="password" class="block text-sm font-medium text-slate-300 mb-1">
|
||||
密码 {{ isEdit ? '(留空则不修改)' : '' }}
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isPasswordBootstrapMode" class="space-y-2 rounded-lg border border-emerald-900/70 bg-emerald-950/30 p-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-slate-100">创建后将自动切换为私钥认证</p>
|
||||
<p class="mt-1 text-xs leading-5 text-slate-400">
|
||||
系统会生成一把新的 SSH 密钥,先用密码登录并写入远端 <code>authorized_keys</code>,验证成功后再保存连接。
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label for="bootstrapPassword" class="block text-sm font-medium text-slate-300 mb-1">初始登录密码</label>
|
||||
<input
|
||||
id="bootstrapPassword"
|
||||
v-model="bootstrapPassword"
|
||||
type="password"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-slate-700 border border-slate-600 text-slate-100 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isPasswordBootstrapMode && authType === 'PASSWORD'">
|
||||
<label for="password" class="block text-sm font-medium text-slate-300 mb-1">
|
||||
密码 {{ isEdit ? '(留空则不修改)' : '' }}
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
v-model="password"
|
||||
type="password"
|
||||
class="w-full px-4 py-2.5 rounded-lg bg-slate-700 border border-slate-600 text-slate-100 focus:outline-none focus:ring-2 focus:ring-cyan-500"
|
||||
:placeholder="isEdit ? '••••••••' : ''"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="authType === 'PRIVATE_KEY'" class="space-y-2">
|
||||
:placeholder="isEdit ? '••••••••' : ''"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="!isPasswordBootstrapMode && authType === 'PRIVATE_KEY'" class="space-y-2">
|
||||
<label for="privateKey" class="block text-sm font-medium text-slate-300 mb-1">
|
||||
私钥 {{ isEdit ? '(留空则不修改)' : '' }}
|
||||
</label>
|
||||
|
||||
@@ -252,7 +252,7 @@ async function handleSessionSubmit(data: ConnectionCreateRequest) {
|
||||
await connectionsStore.createConnection(data)
|
||||
// Tree node insertion is handled by useConnectionSync -> syncNewConnections.
|
||||
// Avoid manual insertion here to prevent duplicate nodes.
|
||||
toast.success('连接已创建')
|
||||
toast.success(data.setupMode === 'PASSWORD_BOOTSTRAP' ? '连接已创建并完成免密配置' : '连接已创建')
|
||||
showFirstRunGuide.value = false
|
||||
}
|
||||
closeSessionModal()
|
||||
|
||||
Reference in New Issue
Block a user