合并服务器配置与SSH密钥引导流程
This commit is contained in:
parent
1b3677d95b
commit
5f4d524b98
@ -38,6 +38,7 @@ export function useDashboard() {
|
||||
const serverSaving = ref(false);
|
||||
const serverFormError = ref("");
|
||||
const showSSHKeyForm = ref(false);
|
||||
const showEmbeddedSSHKeyGuide = ref(false);
|
||||
const sshKeySaving = ref(false);
|
||||
const sshKeyFormError = ref("");
|
||||
const sshKeyPlatform = ref("windows");
|
||||
@ -310,6 +311,15 @@ export function useDashboard() {
|
||||
const serverForm = ref(createEmptyServerForm());
|
||||
const sshKeyForm = ref(createEmptySSHKeyForm());
|
||||
|
||||
watch(
|
||||
() => [serverForm.value.username, serverForm.value.address],
|
||||
([username, address]) => {
|
||||
if (!showEmbeddedSSHKeyGuide.value) return;
|
||||
sshKeyCommandUsername.value = String(username || "").trim() || "用户名";
|
||||
sshKeyCommandHost.value = String(address || "").trim() || "服务器地址";
|
||||
},
|
||||
);
|
||||
|
||||
async function fetchAdmin(path, options = {}) {
|
||||
const response = await fetch(path, {
|
||||
...options,
|
||||
@ -583,10 +593,13 @@ export function useDashboard() {
|
||||
}
|
||||
|
||||
function closeServerForm() {
|
||||
if (!serverSaving.value) showServerForm.value = false;
|
||||
if (!serverSaving.value) {
|
||||
showEmbeddedSSHKeyGuide.value = false;
|
||||
showServerForm.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function openSSHKeyCreate() {
|
||||
function openSSHKeyCreate(options = {}) {
|
||||
sshKeyForm.value = createEmptySSHKeyForm();
|
||||
sshKeyFormError.value = "";
|
||||
sshKeyPlatform.value = "windows";
|
||||
@ -595,7 +608,8 @@ export function useDashboard() {
|
||||
sshKeyCommandHost.value = serverForm.value.address.trim() || "服务器地址";
|
||||
sshKeyRandomSuffix.value = createSSHKeyRandomSuffix();
|
||||
sshKeyForm.value.name = sshKeyFileName.value;
|
||||
showSSHKeyForm.value = true;
|
||||
showEmbeddedSSHKeyGuide.value = options.embedded === true;
|
||||
showSSHKeyForm.value = options.embedded !== true;
|
||||
}
|
||||
|
||||
function closeSSHKeyForm() {
|
||||
@ -603,6 +617,7 @@ export function useDashboard() {
|
||||
sshKeyForm.value = createEmptySSHKeyForm();
|
||||
sshKeyFormError.value = "";
|
||||
sshKeyPassphraseVisible.value = false;
|
||||
showEmbeddedSSHKeyGuide.value = false;
|
||||
showSSHKeyForm.value = false;
|
||||
}
|
||||
|
||||
@ -641,7 +656,11 @@ export function useDashboard() {
|
||||
});
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok) throw new Error(result.error || "保存 SSH 私钥失败");
|
||||
if (showEmbeddedSSHKeyGuide.value && result.reference) {
|
||||
serverForm.value.private_key_ref = result.reference;
|
||||
}
|
||||
sshKeyForm.value = createEmptySSHKeyForm();
|
||||
showEmbeddedSSHKeyGuide.value = false;
|
||||
showSSHKeyForm.value = false;
|
||||
notice.value = "SSH 私钥已加密保存";
|
||||
await loadData();
|
||||
@ -1273,6 +1292,7 @@ export function useDashboard() {
|
||||
serverSaving,
|
||||
serverFormError,
|
||||
showSSHKeyForm,
|
||||
showEmbeddedSSHKeyGuide,
|
||||
sshKeySaving,
|
||||
sshKeyFormError,
|
||||
certificateTypeLabels,
|
||||
|
||||
@ -539,6 +539,23 @@ test("serverModal.compilesAndPreservesDeploymentFields", async () => {
|
||||
},
|
||||
sshKeys: [],
|
||||
openSSHKeyCreate: () => {},
|
||||
showEmbeddedSSHKeyGuide: false,
|
||||
closeSSHKeyForm: () => {},
|
||||
sshKeySaving: false,
|
||||
sshKeyFormError: "",
|
||||
sshKeyPlatform: "windows",
|
||||
sshKeyFileName: "test-key",
|
||||
sshKeyPrivatePath: "/test/private",
|
||||
sshKeyPublicPath: "/test/public",
|
||||
sshKeyCommandUsername: "deploy",
|
||||
sshKeyCommandHost: "example.test",
|
||||
sshKeyCommands: { windows: "generate-windows", linux: "generate-linux" },
|
||||
sshPublicKeyCommands: { windows: "install-windows", linux: "install-linux" },
|
||||
sshKeyForm: { name: "", private_key: "", passphrase: "" },
|
||||
importSSHKeyFile: () => {},
|
||||
sshKeyPassphraseVisible: false,
|
||||
copySSHKeyCommand: () => {},
|
||||
saveSSHKey: () => {},
|
||||
defaultCertificatePath: "",
|
||||
defaultCertificateChainPath: "",
|
||||
defaultPrivateKeyPath: "",
|
||||
|
||||
@ -74,6 +74,161 @@
|
||||
border-top: 1px solid #dce6f1;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide {
|
||||
grid-column: 1 / -1;
|
||||
min-width: 0;
|
||||
margin: 2px 0 8px;
|
||||
padding: 14px;
|
||||
border: 1px solid #c7daf1;
|
||||
border-radius: 14px;
|
||||
background: #f5f9ff;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-heading h3 {
|
||||
margin: 2px 0 4px;
|
||||
color: #214d86;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-steps article {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #d1e0f1;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
color: #294a73;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-steps b {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
flex: 0 0 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: #5b91dc;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-platforms {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 4px;
|
||||
margin-bottom: 8px;
|
||||
padding: 4px;
|
||||
border-radius: 10px;
|
||||
background: #e7effa;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .platform-tab {
|
||||
min-height: 34px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: #5d7390;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .platform-tab.active {
|
||||
background: #fff;
|
||||
color: #24579a;
|
||||
box-shadow: 0 2px 8px rgb(42 80 125 / 12%);
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-path-line,
|
||||
.server-modal .server-ssh-key-guide .ssh-key-path-summary {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #d1e0f1;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-path-line {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-path-summary {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-path-summary > div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide code {
|
||||
overflow-wrap: anywhere;
|
||||
color: #294a73;
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-command-card {
|
||||
margin-bottom: 8px;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #d1e0f1;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-command-heading,
|
||||
.server-modal .server-ssh-key-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-command-heading {
|
||||
margin-bottom: 5px;
|
||||
color: #294a73;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-fields {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0 14px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-fields .field:nth-child(3) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-fields textarea {
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-actions {
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid #d1e0f1;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.server-modal form {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@ -86,6 +241,10 @@
|
||||
.server-modal .server-paths {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-path-summary {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@ -102,4 +261,23 @@
|
||||
.server-modal form > .field {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-steps,
|
||||
.server-modal .server-ssh-key-fields {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-fields .field:nth-child(3) {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-guide .ssh-key-path-line {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.server-modal .server-ssh-key-heading,
|
||||
.server-modal .server-ssh-key-actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,23 @@ const {
|
||||
serverForm,
|
||||
sshKeys,
|
||||
openSSHKeyCreate,
|
||||
closeSSHKeyForm,
|
||||
showEmbeddedSSHKeyGuide,
|
||||
sshKeySaving,
|
||||
sshKeyFormError,
|
||||
sshKeyPlatform,
|
||||
sshKeyFileName,
|
||||
sshKeyPrivatePath,
|
||||
sshKeyPublicPath,
|
||||
sshKeyCommandUsername,
|
||||
sshKeyCommandHost,
|
||||
sshKeyCommands,
|
||||
sshPublicKeyCommands,
|
||||
sshKeyForm,
|
||||
importSSHKeyFile,
|
||||
sshKeyPassphraseVisible,
|
||||
copySSHKeyCommand,
|
||||
saveSSHKey,
|
||||
defaultCertificatePath,
|
||||
defaultCertificateChainPath,
|
||||
defaultPrivateKeyPath,
|
||||
@ -113,7 +130,7 @@ const {
|
||||
><button
|
||||
class="inline-button"
|
||||
type="button"
|
||||
@click="openSSHKeyCreate"
|
||||
@click="openSSHKeyCreate({ embedded: true })"
|
||||
>
|
||||
新建或导入
|
||||
</button></span
|
||||
@ -131,6 +148,139 @@ const {
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
<section v-if="showEmbeddedSSHKeyGuide" class="server-ssh-key-guide">
|
||||
<div class="server-ssh-key-heading">
|
||||
<div>
|
||||
<p class="eyebrow">当前服务器</p>
|
||||
<h3>在这里准备 SSH 私钥</h3>
|
||||
<span class="muted">准备完成后会自动选中,不需要离开服务器配置。</span>
|
||||
</div>
|
||||
<button class="inline-button" type="button" @click="closeSSHKeyForm">
|
||||
收起引导
|
||||
</button>
|
||||
</div>
|
||||
<div class="ssh-key-steps">
|
||||
<article>
|
||||
<b>1</b>
|
||||
<strong>本机生成密钥</strong>
|
||||
</article>
|
||||
<article>
|
||||
<b>2</b>
|
||||
<strong>把公钥放到当前服务器</strong>
|
||||
</article>
|
||||
<article>
|
||||
<b>3</b>
|
||||
<strong>读取私钥并保存</strong>
|
||||
</article>
|
||||
</div>
|
||||
<div class="ssh-key-platforms" role="tablist" aria-label="密钥生成平台">
|
||||
<button
|
||||
class="platform-tab"
|
||||
:class="{ active: sshKeyPlatform === 'windows' }"
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="sshKeyPlatform === 'windows'"
|
||||
@click="sshKeyPlatform = 'windows'"
|
||||
>
|
||||
Windows
|
||||
</button>
|
||||
<button
|
||||
class="platform-tab"
|
||||
:class="{ active: sshKeyPlatform === 'linux' }"
|
||||
type="button"
|
||||
role="tab"
|
||||
:aria-selected="sshKeyPlatform === 'linux'"
|
||||
@click="sshKeyPlatform = 'linux'"
|
||||
>
|
||||
Linux / macOS
|
||||
</button>
|
||||
</div>
|
||||
<div class="ssh-key-path-line">
|
||||
<span>本次密钥文件名</span>
|
||||
<code>{{ sshKeyFileName }}</code>
|
||||
<span class="guide-badge">每次随机生成</span>
|
||||
</div>
|
||||
<div class="ssh-key-path-summary">
|
||||
<div>
|
||||
<span>本地私钥位置</span>
|
||||
<code>{{ sshKeyPrivatePath }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span>本地公钥位置</span>
|
||||
<code>{{ sshKeyPublicPath }}</code>
|
||||
</div>
|
||||
<div>
|
||||
<span>远程登录目标</span>
|
||||
<code>{{ sshKeyCommandUsername }}@{{ sshKeyCommandHost }}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ssh-command-card">
|
||||
<div class="ssh-command-heading">
|
||||
<span>生成命令</span>
|
||||
<button
|
||||
class="inline-button"
|
||||
type="button"
|
||||
@click="copySSHKeyCommand(sshKeyCommands[sshKeyPlatform], '密钥生成命令已复制')"
|
||||
>
|
||||
复制命令
|
||||
</button>
|
||||
</div>
|
||||
<code>{{ sshKeyCommands[sshKeyPlatform] }}</code>
|
||||
</div>
|
||||
<div class="ssh-command-card">
|
||||
<div class="ssh-command-heading">
|
||||
<span>安装公钥命令</span>
|
||||
<button
|
||||
class="inline-button"
|
||||
type="button"
|
||||
@click="copySSHKeyCommand(sshPublicKeyCommands[sshKeyPlatform], '公钥安装命令已复制')"
|
||||
>
|
||||
复制命令
|
||||
</button>
|
||||
</div>
|
||||
<code>{{ sshPublicKeyCommands[sshKeyPlatform] }}</code>
|
||||
</div>
|
||||
<div class="server-ssh-key-fields">
|
||||
<label class="field">
|
||||
<span>私钥名称</span>
|
||||
<input v-model="sshKeyForm.name" required type="text" maxlength="128" autocomplete="off" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>读取本机私钥文件</span>
|
||||
<input type="file" @change="importSSHKeyFile" />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>私钥内容</span>
|
||||
<textarea
|
||||
v-model="sshKeyForm.private_key"
|
||||
rows="4"
|
||||
spellcheck="false"
|
||||
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
|
||||
></textarea>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span class="field-label-row">
|
||||
<span>私钥口令</span>
|
||||
<button class="inline-button" type="button" @click="sshKeyPassphraseVisible = !sshKeyPassphraseVisible">
|
||||
{{ sshKeyPassphraseVisible ? "隐藏口令" : "显示口令" }}
|
||||
</button>
|
||||
</span>
|
||||
<input
|
||||
v-model="sshKeyForm.passphrase"
|
||||
:type="sshKeyPassphraseVisible ? 'text' : 'password'"
|
||||
maxlength="4096"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<p v-if="sshKeyFormError" class="form-error">{{ sshKeyFormError }}</p>
|
||||
<div class="server-ssh-key-actions">
|
||||
<span class="muted">私钥只会加密保存,服务器配置仅保存安全引用。</span>
|
||||
<button class="primary-button" type="button" :disabled="sshKeySaving" @click="saveSSHKey">
|
||||
{{ sshKeySaving ? "加密保存中……" : "加密保存并用于当前服务器" }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<label class="field"
|
||||
><span>SSH 主机 SHA256 指纹</span
|
||||
><input
|
||||
|
||||
Loading…
Reference in New Issue
Block a user