完善初始化页面、代理配置与远程部署文档

This commit is contained in:
xuans 2026-08-29 20:43:41 +08:00
parent f85ef10527
commit afdb947bea
9 changed files with 731 additions and 15 deletions

View File

@ -0,0 +1,171 @@
# SSH 密钥配置指南
本文用于帮助不熟悉 SSH 的用户完成 Scert 远程服务器部署配置。Scert 只使用 SSH 密钥认证,不保存或使用远程服务器登录密码。
## 一、配置流程
SSH 配置分为三步:
1. 在自己的电脑上生成一对 SSH 密钥。
2. 将公钥安装到远程服务器的授权文件中。
3. 将本机私钥录入 Scert,Scert 会在保存时加密存储。
密钥分为两部分:
- **私钥**:只保存在自己的电脑或 Scert 的加密敏感信息存储中,不能发送给他人,不能提交到 Git。
- **公钥**:可以安装到远程服务器,用于验证私钥对应的客户端。
## 二、Windows 生成密钥
Windows 10、Windows 11 和较新的 Windows Server 通常已经包含 OpenSSH 客户端。打开 PowerShell,复制并执行:
```powershell
ssh-keygen -t ed25519 -C "scert-deploy" -f "$HOME\.ssh\scert_deploy_ed25519"
```
执行过程中:
1. 如果提示创建目录,输入 `Y` 或按回车确认。
2. 如果提示输入私钥口令,可以设置一个口令;如果希望配置更简单,也可以直接按两次回车留空。
3. 私钥默认保存为:`%USERPROFILE%\.ssh\scert_deploy_ed25519`。
4. 公钥默认保存为:`%USERPROFILE%\.ssh\scert_deploy_ed25519.pub`。
查看公钥内容:
```powershell
Get-Content "$HOME\.ssh\scert_deploy_ed25519.pub"
```
复制公钥内容时,要从 `ssh-ed25519` 开始复制到末尾的注释,必须保持为一整行。
## 三、Linux 或 macOS 生成密钥
打开终端,复制并执行:
```bash
ssh-keygen -t ed25519 -C "scert-deploy" -f ~/.ssh/scert_deploy_ed25519
```
查看公钥内容:
```bash
cat ~/.ssh/scert_deploy_ed25519.pub
```
默认文件位置:
- 私钥:`~/.ssh/scert_deploy_ed25519`
- 公钥:`~/.ssh/scert_deploy_ed25519.pub`
建议设置本地私钥权限:
```bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/scert_deploy_ed25519
chmod 644 ~/.ssh/scert_deploy_ed25519.pub
```
## 四、把公钥安装到 Linux 服务器
如果本机安装了 `ssh-copy-id`,可以执行:
```bash
ssh-copy-id -i ~/.ssh/scert_deploy_ed25519.pub 用户名@服务器地址
```
命令会要求输入一次服务器登录密码。密码只用于把公钥安装到服务器,不会提交给 Scert。
如果没有 `ssh-copy-id`,先查看公钥:
```bash
cat ~/.ssh/scert_deploy_ed25519.pub
```
然后使用服务器控制台或已有登录方式,在远程服务器执行:
```bash
umask 077
mkdir -p ~/.ssh
cat >> ~/.ssh/authorized_keys
```
执行后将公钥整行粘贴进去,按回车,再按 `Ctrl+D` 结束输入。最后设置权限:
```bash
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
```
如果远程服务器使用的是其他登录用户,必须把公钥放到该用户自己的 `~/.ssh/authorized_keys`,不能误放到 root 用户目录。
## 五、把公钥安装到 Windows OpenSSH 服务器
Windows 服务器需要先确认 OpenSSH Server 已安装并运行。对于普通用户,将公钥追加到该用户的 `.ssh\authorized_keys` 文件;对于管理员账户,Windows OpenSSH 通常使用系统级管理员授权文件,具体位置以服务器 OpenSSH 配置为准。
建议优先使用服务器管理员提供的 OpenSSH 配置方式,不要直接覆盖已有授权文件。追加公钥时只能新增一行,不能删除其他用户已经配置的公钥。
## 六、在 Scert 中录入私钥
在 Scert 的服务器配置页面:
1. 点击“新建或导入”。
2. 选择自己的操作系统标签,查看对应的密钥生成和公钥安装命令。
3. 在“私钥名称”中填写容易识别的名称,例如“测试服务器部署密钥”。
4. 可以直接把私钥内容粘贴到“私钥内容”,也可以点击文件选择框读取本机私钥文件。
5. 如果生成密钥时设置了口令,在“私钥口令”中填写相同口令;未设置口令时留空。
6. 确认内容无误后点击“加密保存”。
7. 返回服务器配置,选择刚刚保存的 SSH 私钥。
读取本机私钥文件只会把内容放入当前页面内存,仍需点击“加密保存”才会提交。关闭窗口或刷新页面前,请确认已经完成保存。
## 七、测试连接前检查
配置服务器时需要填写:
- 服务器地址和 SSH 端口,默认端口为 `22`。
- SSH 用户名。
- 已加密保存的 SSH 私钥。
- SSH 主机 `SHA256` 指纹。
- 远程证书、完整证书链和私钥路径。
首次连接前,请通过可信的服务器控制台或管理员渠道核对主机指纹。不要为了绕过连接失败而随意删除指纹校验。
测试环境当前包括:
- `192.168.2.3`
- `192.168.2.4`
上述地址仅用于授权测试,不应直接写入生产配置。生产环境应由用户自行填写实际服务器地址,并重新核对主机指纹和远程路径。
## 八、常见问题
### 1. 提示权限不足
检查远程用户是否有权读取或替换证书目录,以及是否有权执行 Nginx、Caddy 或 Apache 的重载命令。不要直接给服务账号授予不必要的全盘权限。
### 2. 提示私钥格式错误
请确认录入的是私钥文件,而不是 `.pub` 公钥文件。私钥内容通常以 `BEGIN` 开头;公钥通常以 `ssh-ed25519`、`ssh-rsa` 或类似标识开头。
### 3. 设置了私钥口令但连接失败
检查 Scert 中填写的私钥口令是否与生成密钥时设置的口令完全一致。配置页面允许明文显示口令,便于确认输入;保存后由后端加密存储,接口不返回明文。
### 4. 不知道怎么操作
最简单的方式是:
1. 在 Scert 弹窗中复制密钥生成命令。
2. 在自己的电脑上执行命令。
3. 将公钥安装命令中的服务器地址和用户名替换为实际值并执行。
4. 回到 Scert,选择刚生成的私钥文件。
5. 点击“加密保存”,再配置服务器。
## 九、安全注意事项
- 不要把私钥发送给任何人。
- 不要把私钥、公钥安装密码或 DNS 密钥提交到 Git。
- 不要把私钥粘贴到远程服务器;远程服务器只需要公钥。
- 不要覆盖远程服务器已有的 `authorized_keys` 文件。
- 服务器地址、端口、用户和主机指纹应按环境分别配置。
- 测试机的配置不能直接复制到生产环境。

View File

@ -996,3 +996,28 @@ imported 外部导入
- `[x]` 本机防火墙自动配置已完成代码和自动化测试;安装器提权与各目标系统实机复审待完成。
- `[!]` 云安全组自动配置不纳入当前自动修改范围,继续由初始化界面提示用户手动放行。
- `[!]` 公网 IP 证书在目标 CA 和 `lego` 版本上的兼容性验证。
## 三十、本轮 DNS 授权范围与 SSH 推送测试范围补充(2026-08-29)
本节记录本轮用户补充内容,保留前文历史测试记录,便于对照追踪。
### 30.1 阿里云 RAM DNS 授权范围
- **用户补充:**本轮测试使用的阿里云 RAM 账号只允许控制以下 DNS 根域名:
- `epwn.uno`
- `zedyj.cn`
- `szqh.uno`
- **实现说明:**测试代码、示例配置和文档不得保存 AccessKeyId、AccessKeySecret 或其他敏感凭据;DNS 测试应验证授权边界,禁止将操作扩展到未授权域名。
- **状态:**`[x]` 已记录最新授权域名范围,真实 API 读取验证待在授权环境执行。
### 30.2 SSH 推送测试服务器
- **用户补充:**在已有 `192.168.2.3` 测试机基础上,增加 `192.168.2.4` 作为证书推送测试机。
- **实现说明:**测试范围同时覆盖 `192.168.2.3` 和 `192.168.2.4`;测试配置不得硬编码到生产配置,且每台服务器必须单独配置 SSH 用户、端口、私钥引用、主机指纹和远程证书路径。
- **状态:**`[x]` 已更新测试范围说明,实际连接、证书推送和服务重载验证待执行。
### 30.3 SSH 密钥配置引导
- **用户补充:**为不熟悉 SSH 密钥生成的用户提供从生成密钥到粘贴公钥的完整指导,并支持更省事的操作方式。
- **实现说明:**服务器配置页面增加 Windows、Linux/macOS 命令切换;支持复制密钥生成命令、复制公钥安装命令、选择本机私钥文件导入、私钥口令显示/隐藏,以及 Linux 和 Windows OpenSSH 授权位置说明。私钥只在页面内存中读取,保存时由后端加密存储。
- **状态:**`[x]` 已完成前端引导和中文说明文档,待前端构建和真实浏览器页面复审。

View File

@ -2582,4 +2582,131 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无
### 59.3 当前状态
本轮代码调整和自动化检查已完成,相关任务标记为“已完成,等待复审”。浏览器 MCP 视觉验收属于外部运行环境阻塞,不影响前端构建和后端自动化检查结果;待独立浏览器窗口可用后,再对桌面宽屏、中等窗口和低高度窗口进行实际显示复审。
本轮代码调整和自动化检查已完成,相关任务标记为“已完成,等待复审”。浏览器 MCP 视觉验收属于外部运行环境阻塞,不影响前端构建和后端自动化检查结果;待独立浏览器窗口可用后,再对桌面宽屏、中等窗口和低高度窗口进行实际显示复审。
## 六十、SSH 密钥配置引导与最新测试范围更新(2026-08-29)
### 60.1 需求清单
- [x] 记录最新阿里云 RAM 测试授权域名:`epwn.uno`、`zedyj.cn`、`szqh.uno`。
- [x] 将 `192.168.2.4` 加入 SSH 推送测试范围,并保留 `192.168.2.3`。
- [x] 为 SSH 密钥配置增加 Windows、Linux/macOS 生成命令引导。
- [x] 增加公钥安装位置和安装命令说明。
- [x] 增加复制密钥生成命令和公钥安装命令功能。
- [x] 增加本机私钥文件导入功能,导入内容只进入当前页面内存。
- [x] 增加私钥口令显示/隐藏功能,并保持后端加密存储约束。
- [x] 保持 SSH 主机 SHA256 指纹校验和私钥不回显策略。
### 60.2 实现文件
- `frontend/src/views/Dashboard/Dashboard.js`
- 增加 SSH 平台切换、命令模板、剪贴板复制和本机私钥文件读取逻辑。
- `frontend/src/views/Dashboard/Dashboard.vue`
- 增加 SSH 三步配置引导、平台选择、命令复制、私钥文件导入和口令显示控制。
- `frontend/src/views/Dashboard/Dashboard.css`
- 增加引导卡片、步骤布局、命令卡片、平台标签和文件选择控件样式。
- `doc/SSH密钥配置指南.md`
- 增加用户可直接照做的 Windows、Linux/macOS、Linux 服务器和 Windows OpenSSH 配置说明。
### 60.3 待验证任务
- [ ] 使用最新授权域名完成阿里云 DNS API 读取边界验证,不记录或输出敏感凭据。
- [ ] 在 `192.168.2.3` 上完成 SSH 连接、证书文件推送和网站服务重载测试。
- [ ] 在 `192.168.2.4` 上完成 SSH 连接、证书文件推送和网站服务重载测试。
- [ ] 完成前端生产构建、后端竞态测试、静态检查和模块完整性检查。
- [ ] 在独立浏览器 MCP 窗口中完成桌面端页面截图复审。
- [!] 移动端视觉复审按规则使用 Mobile Preview 插件,不在本轮启动浏览器 MCP 移动端预览。
### 60.4 安全边界
- 不把阿里云 RAM 凭据、SSH 私钥、服务器密码写入代码、文档、普通配置或 Git。
- 不把测试服务器地址硬编码到生产配置。
- 不覆盖远程服务器已有授权公钥,只允许追加或由用户明确维护授权文件。
- SSH 连接仍必须校验主机 SHA256 指纹。
### 60.5 本轮验证结果
- [x] `npm.cmd run build` 通过,前端生产构建成功。
- [x] `go test -race ./...` 通过。
- [x] `go vet ./...` 通过。
- [x] `go mod verify` 通过,所有模块已验证。
- [x] `git diff --check` 通过。
- [x] 本机前端 `127.0.0.1:5273` 返回 HTTP 200。
- [x] Scert 后端 `127.0.0.1:5200/healthz` 返回 HTTP 200,健康状态正常。
- [x] `192.168.2.3:22` 和 `192.168.2.4:22` TCP 可达。
- [!] 尚未执行两台测试机的 SSH 登录、证书推送和网站服务重载;需要分别配置并核对私钥和主机指纹。
- [!] 浏览器 MCP 默认共享 Profile 仍被其他浏览器进程占用,本轮未强制关闭现有浏览器,未完成真实截图复审。
### 60.6 复审状态
- `[x]` 源码和文档修改完成,等待用户复审。
- `[x]` 自动化构建和后端检查完成,等待用户复审。
- `[!]` 真实 SSH 推送、DNS API 在线验证和浏览器截图复审仍待独立测试条件满足后执行。
## 六十一、最新授权域名 DNS API 在线测试(2026-08-29)
### 61.1 测试范围
- [x] 使用当前源码启动独立测试实例,监听 `127.0.0.1:5201`,避免影响仍在运行的 `5200` 旧版本实例。
- [x] 使用阿里云 RAM 测试账号验证 `epwn.uno` 的 DNS API 操作能力。
- [x] 使用阿里云 RAM 测试账号验证 `zedyj.cn` 的 DNS API 操作能力。
- [x] 使用阿里云 RAM 测试账号验证 `szqh.uno` 的 DNS API 操作能力。
- [x] 不申请证书,仅通过测试接口创建并清理临时 DNS-01 验证记录。
### 61.2 测试结果
| 测试域名 | 测试接口结果 | 结论 |
|---|---:|---|
| `epwn.uno` | HTTP 200,验证通过 | 阿里云 DNS API 可用 |
| `zedyj.cn` | HTTP 200,验证通过 | 阿里云 DNS API 可用 |
| `szqh.uno` | HTTP 200,验证通过 | 阿里云 DNS API 可用 |
- [x] 当前源码 DNS Provider 列表中,阿里云 DNS 显示为可用。
- [x] 三个域名均完成临时记录创建和自动清理流程。
- [x] 测试实例配置文件中未发现明文 AccessKeySecret。
- [x] 未将任何 AccessKey、Secret 或测试凭据写入代码、文档或 Git。
### 61.3 其他运行状态
- [x] `http://127.0.0.1:5201/healthz` 返回 HTTP 200。
- [x] 原有 `http://127.0.0.1:5200/healthz` 返回 HTTP 200;该端口当前仍运行旧构建,不用于本轮源码 Provider 结论。
- [x] 前端 `http://127.0.0.1:5273/` 返回 HTTP 200。
- [x] `192.168.2.3:22` TCP 可达。
- [x] `192.168.2.4:22` TCP 可达。
### 61.4 尚未执行项目
- [!] 尚未进行两台服务器的 SSH 私钥登录;当前缺少可用于测试的私钥和已核对的主机指纹。
- [!] 尚未向两台服务器推送证书或执行 Nginx、Caddy、Apache 服务重载。
- [!] 浏览器 MCP 共享 Chrome Profile 仍被其他浏览器进程占用,尚未完成桌面端真实截图复审。
- [!] 移动端视觉测试仍按规则保留给 Mobile Preview 插件,本轮未启动浏览器 MCP 移动端预览。
### 61.5 本轮复审结论
- `[x]` 阿里云 RAM 对三个最新授权根域名的 DNS-01 临时记录操作已完成真实验证。
- `[x]` 当前源码中的阿里云 Provider 已确认不是旧版本的“未实现”状态。
- `[!]` DNS API 验证通过不等同于生产证书签发、远程部署和线上 HTTPS 校验完成;后续仍需在明确 SSH 私钥和主机指纹后执行分阶段部署测试。
## 六十二、绑定邮箱完整地址识别修复(2026-08-29)
### 62.1 用户反馈
- [x] 绑定邮箱输入框不再显示具体邮箱用户名示例。
- [x] 输入完整邮箱地址时,自动识别 `@` 后的邮箱域名。
- [x] 输入框只显示 `@` 前的邮箱名称,下拉列表显示对应常用邮箱后缀。
- [x] 输入到 `@` 但尚未输入域名时,暂时保留当前输入,避免受控输入框重绘导致光标跳动和内容错乱。
### 62.2 修改文件
- `frontend/src/views/SetupWizard/SetupWizard.vue`
- 将绑定邮箱占位文字改为通用提示“请输入邮箱名称”。
- `frontend/src/views/SetupWizard/SetupWizard.js`
- 完整邮箱输入时拆分本地部分和域名。
- 常用域名自动选中对应下拉项。
- 非常用域名自动归入“自定义后缀”。
- 保留完整邮箱到提交数据中的 `admin_email` 字段。
### 62.3 验证结果
- [x] 前端生产构建通过。
- [x] 源码中未发现 `likb_blue` 等具体邮箱示例。
- [x] 源码中未发现 HTML 数字编码中文字符。
- [x] `git diff --check` 通过。
- [!] 浏览器 MCP 当前因共享 Chrome Profile 被其他浏览器进程占用,暂未完成本次实际输入交互截图复审。

View File

@ -472,3 +472,218 @@ code {
gap: 0;
}
}
.field-label-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.field-label-row > span:first-child {
min-width: 0;
}
.inline-button {
flex: 0 0 auto;
border: 0;
padding: 4px 8px;
border-radius: 6px;
color: #2557a7;
background: #edf4ff;
font-size: 12px;
line-height: 1.4;
cursor: pointer;
}
.inline-button:hover {
color: #17458f;
background: #dceaff;
}
.ssh-key-guide {
margin: 0 0 18px;
padding: 16px;
border: 1px solid #dbe7f7;
border-radius: 12px;
background: linear-gradient(135deg, #f7fbff, #f3f7fc);
}
.ssh-key-guide-heading {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 14px;
color: #183b68;
font-size: 14px;
font-weight: 700;
}
.ssh-key-guide-heading::before {
content: "K";
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 7px;
color: #ffffff;
background: #3677d8;
font-size: 12px;
}
.ssh-key-steps {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin: 0;
}
.ssh-key-steps article {
display: flex;
gap: 9px;
min-width: 0;
padding: 11px;
border-radius: 9px;
background: #ffffff;
}
.ssh-key-steps b {
display: inline-flex;
flex: 0 0 22px;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
color: #ffffff;
background: #5f93dc;
font-size: 12px;
}
.ssh-key-steps div {
display: grid;
gap: 3px;
min-width: 0;
}
.ssh-key-steps strong {
color: #23466f;
font-size: 12px;
}
.ssh-key-steps span {
color: #687b94;
font-size: 11px;
line-height: 1.55;
}
.ssh-key-platforms {
display: flex;
gap: 6px;
margin: 16px 0 10px;
padding: 4px;
border-radius: 8px;
background: #e8eff9;
}
.platform-tab {
flex: 1;
border: 0;
padding: 8px 10px;
border-radius: 6px;
color: #55708e;
background: transparent;
font-size: 12px;
cursor: pointer;
}
.platform-tab.active {
color: #214f98;
background: #ffffff;
box-shadow: 0 2px 8px rgba(48, 83, 132, 0.12);
font-weight: 700;
}
.ssh-command-card {
margin-top: 10px;
padding: 11px 12px;
border: 1px solid #dfe8f3;
border-radius: 9px;
background: #ffffff;
}
.ssh-command-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 7px;
color: #3b5778;
font-size: 12px;
font-weight: 700;
}
.ssh-command-card > code {
display: block;
overflow-wrap: anywhere;
color: #294765;
font-family: Consolas, "SFMono-Regular", monospace;
font-size: 11px;
line-height: 1.65;
white-space: pre-wrap;
}
.ssh-command-card small {
display: block;
margin-top: 8px;
color: #71839a;
font-size: 11px;
line-height: 1.55;
}
.ssh-key-guide-note {
margin: 12px 0 0;
color: #637895;
font-size: 11px;
line-height: 1.6;
}
.field input[type="file"] {
padding: 9px;
color: #5d6f86;
background: #f8fafc;
font-size: 12px;
}
@media (max-width: 700px) {
.ssh-key-steps {
grid-template-columns: 1fr;
}
.field-label-row {
align-items: flex-start;
}
}
.ssh-key-guide-heading > div:first-child {
display: grid;
gap: 3px;
min-width: 0;
}
.ssh-key-guide-heading > div:first-child span {
color: #6d819b;
font-size: 11px;
font-weight: 400;
line-height: 1.55;
}
.guide-badge {
flex: 0 0 auto;
padding: 4px 8px;
border-radius: 999px;
color: #2c68ba;
background: #e2efff;
font-size: 11px;
white-space: nowrap;
}

View File

@ -22,6 +22,21 @@ export function useDashboard() {
const showSSHKeyForm = ref(false);
const sshKeySaving = ref(false);
const sshKeyFormError = ref("");
const sshKeyPlatform = ref("windows");
const sshKeyPassphraseVisible = ref(false);
const sshKeyCommands = {
windows:
'ssh-keygen -t ed25519 -C "scert-deploy" -f "$HOME\\.ssh\\scert_deploy_ed25519"',
linux:
'ssh-keygen -t ed25519 -C "scert-deploy" -f ~/.ssh/scert_deploy_ed25519',
};
const sshPublicKeyCommands = {
windows:
'Get-Content "$HOME\\.ssh\\scert_deploy_ed25519.pub" | ssh root@服务器地址 "umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys"',
linux:
'ssh-copy-id -i ~/.ssh/scert_deploy_ed25519.pub 用户名@服务器地址',
};
const certificateTypeLabels = {
single: "单域名",
@ -178,6 +193,8 @@ export function useDashboard() {
function openSSHKeyCreate() {
sshKeyForm.value = createEmptySSHKeyForm();
sshKeyFormError.value = "";
sshKeyPlatform.value = "windows";
sshKeyPassphraseVisible.value = false;
showSSHKeyForm.value = true;
}
@ -185,9 +202,34 @@ export function useDashboard() {
if (sshKeySaving.value) return;
sshKeyForm.value = createEmptySSHKeyForm();
sshKeyFormError.value = "";
sshKeyPassphraseVisible.value = false;
showSSHKeyForm.value = false;
}
async function importSSHKeyFile(event) {
const file = event.target.files?.[0];
event.target.value = "";
if (!file) return;
try {
const content = await file.text();
if (!content.trim()) throw new Error("选择的私钥文件为空");
sshKeyForm.value.private_key = content;
sshKeyFormError.value = "";
notice.value = `已读取私钥文件:${file.name}`;
} catch (err) {
sshKeyFormError.value = err.message || "读取私钥文件失败";
}
}
async function copySSHKeyCommand(value, message) {
try {
await navigator.clipboard.writeText(value);
notice.value = message;
} catch {
sshKeyFormError.value = "浏览器未允许访问剪贴板,请手动复制命令";
}
}
async function saveSSHKey() {
sshKeyFormError.value = "";
sshKeySaving.value = true;
@ -456,6 +498,10 @@ export function useDashboard() {
form,
serverForm,
sshKeyForm,
sshKeyPlatform,
sshKeyPassphraseVisible,
sshKeyCommands,
sshPublicKeyCommands,
defaultCertificatePath,
defaultCertificateChainPath,
defaultPrivateKeyPath,
@ -467,6 +513,8 @@ export function useDashboard() {
closeServerForm,
openSSHKeyCreate,
closeSSHKeyForm,
importSSHKeyFile,
copySSHKeyCommand,
saveSSHKey,
deleteSSHKey,
saveServer,

View File

@ -32,6 +32,10 @@ const {
form,
serverForm,
sshKeyForm,
sshKeyPlatform,
sshKeyPassphraseVisible,
sshKeyCommands,
sshPublicKeyCommands,
defaultCertificatePath,
defaultCertificateChainPath,
defaultPrivateKeyPath,
@ -43,6 +47,8 @@ const {
closeServerForm,
openSSHKeyCreate,
closeSSHKeyForm,
importSSHKeyFile,
copySSHKeyCommand,
saveSSHKey,
deleteSSHKey,
saveServer,
@ -489,7 +495,15 @@ const {
required
type="text" /></label
><label class="field"
><span>SSH 私钥</span
><span class="field-label-row"
><span>SSH 私钥</span
><button
class="inline-button"
type="button"
@click="openSSHKeyCreate"
>
新建或导入
</button></span
><select v-model="serverForm.private_key_ref" required>
<option value="">请选择已加密保存的私钥</option>
<option
@ -500,7 +514,7 @@ const {
{{ key.name }} · {{ key.algorithm }}
</option></select
><small v-if="sshKeys.length === 0"
>请先关闭窗口并添加 SSH 私钥。</small
>还没有私钥?点击“新建或导入”,按照引导生成、粘贴或读取私钥文件。</small
></label
>
</div>
@ -597,6 +611,104 @@ const {
</button>
</div>
<form @submit.prevent="saveSSHKey">
<section class="ssh-key-guide">
<div class="ssh-key-guide-heading">
<div>
<strong>不会准备 SSH 密钥?</strong>
<span>按下面三步操作,完成生成、安装公钥和录入私钥。</span>
</div>
<span class="guide-badge">新手引导</span>
</div>
<div class="ssh-key-steps">
<article>
<b>1</b>
<div>
<strong>在本机生成密钥</strong>
<span>打开 PowerShell、终端或命令提示符,复制下面的命令执行。</span>
</div>
</article>
<article>
<b>2</b>
<div>
<strong>把公钥放到服务器</strong>
<span>公钥放入服务器的授权文件,私钥不要粘贴到服务器。</span>
</div>
</article>
<article>
<b>3</b>
<div>
<strong>录入私钥并保存</strong>
<span>可以直接粘贴,也可以选择本机私钥文件,Scert 会加密保存。</span>
</div>
</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-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>
<small>
Linux 服务器默认位置:<code>~/.ssh/authorized_keys</code>;Windows
OpenSSH 服务器请按照系统说明配置授权文件。
</small>
</div>
<p class="ssh-key-guide-note">
懒人方式:如果你能登录服务器控制台,只需执行生成命令,再把生成的
<code>.pub</code> 公钥内容追加到授权文件,最后在下方选择私钥文件即可。
</p>
</section>
<label class="field"
><span>私钥名称</span
><input
@ -607,6 +719,10 @@ const {
placeholder="例如生产环境部署私钥"
autocomplete="off"
/></label>
<label class="field"
><span>读取本机私钥文件 <small>可选,读取后仍需点击加密保存</small></span
><input type="file" @change="importSSHKeyFile" /></label
>
<label class="field"
><span
>私钥内容
@ -620,10 +736,18 @@ const {
></textarea>
</label>
<label class="field"
><span>私钥口令 <small>未加密私钥请留空</small></span
><span class="field-label-row"
><span>私钥口令 <small>未加密私钥请留空</small></span
><button
class="inline-button"
type="button"
@click="sshKeyPassphraseVisible = !sshKeyPassphraseVisible"
>
{{ sshKeyPassphraseVisible ? "隐藏口令" : "显示口令" }}
</button></span
><input
v-model="sshKeyForm.passphrase"
type="password"
:type="sshKeyPassphraseVisible ? 'text' : 'password'"
maxlength="4096"
autocomplete="new-password"
/></label>
@ -652,4 +776,4 @@ const {
</div>
</main>
</template>
<style scoped src="./Dashboard.css"></style>
<style scoped src="./Dashboard.css"></style>

View File

@ -154,7 +154,7 @@ export function useSetupWizard(props, emit) {
}
const atIndex = rawValue.indexOf("@");
if (atIndex <= 0) {
if (atIndex < 0) {
emailLocalPart.value = rawValue;
form.value.admin_email = selectedEmailDomain.value && selectedEmailDomain.value !== "custom"
? `${rawValue}@${selectedEmailDomain.value}`
@ -164,12 +164,18 @@ export function useSetupWizard(props, emit) {
const localPart = rawValue.slice(0, atIndex).trim();
const domain = rawValue.slice(atIndex + 1).trim().toLowerCase();
if (!localPart || !domain) {
emailLocalPart.value = rawValue;
selectedEmailDomain.value = "";
form.value.admin_email = rawValue;
return;
}
emailLocalPart.value = localPart;
selectedEmailDomain.value = commonEmailDomains.some(
(option) => option.value === domain,
)
? domain
: "custom";
(option) => option.value === domain && option.value !== "custom",
) ? domain : "custom";
form.value.admin_email = `${localPart}@${domain}`;
}

View File

@ -128,7 +128,7 @@ const {
type="text"
autocomplete="email"
inputmode="email"
placeholder="输入邮箱名称,例如 likb_blue"
placeholder="请输入邮箱名称"
/>
<select
id="admin-email-domain"

View File

@ -8,10 +8,10 @@ export default defineConfig({
host: process.env.SCERT_DEV_HOST || '127.0.0.1',
port: Number(process.env.SCERT_DEV_PORT || 5173),
proxy: {
'/api': process.env.SCERT_API_TARGET || 'http://127.0.0.1:8090',
'/healthz': process.env.SCERT_API_TARGET || 'http://127.0.0.1:8090',
'/api': process.env.SCERT_API_TARGET || 'http://127.0.0.1:5200',
'/healthz': process.env.SCERT_API_TARGET || 'http://127.0.0.1:5200',
'/reset': {
target: process.env.SCERT_API_TARGET || 'http://127.0.0.1:8090',
target: process.env.SCERT_API_TARGET || 'http://127.0.0.1:5200',
changeOrigin: false,
bypass(request) {
if (