多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Nginx UI 自签名证书体验增强:StringListInput 共享组件、自动续期提示与 Name 必填校验实现解析

Nginx UI 自签名证书体验增强:StringListInput 共享组件、自动续期提示与 Name 必填校验实现解析 后端前端运维MCP 服务【免费下载链接】nginx-uiYet another WebUI for Nginx项目地址https://gitcode.com/gh_mirrors/ngi/nginx-ui点击查看免费下载导读本文围绕 Nginx UI 仓库中 自签名证书 UX 增强实现计划 与其配套设计规格展开深入讲解三项核心改造将「Custom Domains 多行编辑器」抽象为可复用的StringListInput组件并复用到自签名证书的 Domains / IP Addresses 字段在自签名表单顶部增加自动续期策略提示在前端与后端同时强制自签名证书 Name 必填。读完本文你将掌握该功能模块的组件抽象思路、payload 种子与过滤模式、Go 后端binding:required校验契约以及完整的测试与手工冒烟验证方法。背景一次手工测试暴露的三个体验问题自签名证书创建流程合并进「申请证书Issue Certificate」对话框后Nginx UI 在手工测试中发现三个不一致点构成了本次增强的动因编辑器风格不统一。同一对话框里Custom Domains 分支用的是带「Add/Remove」按钮的多行AInput列表而自签名证书的 Domains 与 IP Addresses 用的是 chip 风格的 tags 下拉ASelect modetags。同样的输入语义两种编辑模型。缺少自动续期上下文提示。用户在创建自签名证书时表单内没有任何提示说明 Nginx UI 会自动续期而续期策略取决于全局设置与单张证书的有效期用户无从知晓。空 Name 可静默通过。后端此前对空Name不报错导致证书列表出现空白行且文件系统 slug 只能回退到第一个域名或 IP。对自签名证书而言Name 应当必填。设计文档还明确划定了非目标Non-goals避免改造范围失控不做前端 IP 格式校验后端已通过binding:omitempty,dive,ip兜底不迁移已存在的空 Name 历史数据用户重新打开并保存时新校验自然生效不改名DNSIssueCertificate.vue不改变 ACMEWildcard / Custom Domains提交语义。总体架构与文件地图方案的核心分层非常清晰前端组件复用 三处消费方接线 请求边界校验。新增文件 2 个文件职责app/src/components/StringListInput/StringListInput.vue可复用的多行字符串数组输入Add / Removeapp/src/components/StringListInput/index.ts组件再导出入口修改文件 9 个文件改动app/src/views/certificate/components/DNSIssueCertificate.vueCustom Domains 改用StringListInputsubmitSelfSigned裁剪过滤并校验 Namepayload 种子[]app/src/views/certificate/components/SelfSignedCertFields.vueDomains / IPs 改用StringListInput新增续期AAlert带hideRenewalNote开关Name 必填app/src/views/certificate/components/SelfSignedCertForm.vueemptyForm()种子空行数组submit()裁剪过滤并校验 Nameapp/src/views/certificate/components/SelfSignedCertManagement.vue传hide-renewal-note避免双重提示app/src/views/certificate/CertificateEditor.vuesave()的isSelfSigned分支裁剪过滤并校验 Nameapp/src/api/cert.tstoSelfSignedPayload()种子空行数组api/certificate/self_signed.goSelfSignedCertRequest.Name增加binding:requiredapi/certificate/self_signed_test.go新增空 Name 返回 4xx 的接口测试另有明确不修改的文件app/src/views/site/site_edit/components/Cert/SelfSignedCert.vue它以defaultDomains调用SelfSignedCertForm下游改动自动传播以及 internal/cert/self_signed.go 等后端辅助代码校验统一收敛在请求边界。组件层StringListInput 的设计与实现组件 APIinterface Props { placeholder?: string addButtonText?: string // 默认: $gettext(Add Item) // v1 不提供 validator prop —— YAGNI }v-model的类型是string[]必填。设计上允许数组内含单个空字符串——这是用户在输入第一个值时的正常中间状态消费方负责在提交时过滤空值。组件行为依据 StringListInput.vue 的实现行为可归纳为每个数组元素渲染一个AInput当数组长度大于 1 时每行右侧显示红色链接按钮Remove点击后按索引splice删除该行底部一个 block 级AButton文案由addButtonText控制默认Add Item点击向数组push()追加空行组件不做任何内部校验仅用:placeholder提供输入提示。关键实现细节与计划文档的说明一致仓库代码已落地script setup langts defineProps{ placeholder?: string addButtonText?: string }() const items defineModelstring[]({ required: true }) function addItem() { items.value [...items.value, ] } function removeItem(index: number) { if (items.value.length 1) return const next [...items.value] next.splice(index, 1) items.value next } /script两点值得注意使用defineModelstring[]({ required: true })这是 Vue 3.4 的双向绑定惯用法项目内已有先例删除/追加/更新都通过展开数组生成新引用[...items.value]再写回 model保持响应式身份稳定让v-model干净地 emit——这是相对原先「通过双向绑定直接改customDomains[index]」的显式 setter 化改造。从源码结构看实际仓库中的组件实现StringListInput.vue相比计划草案还增加了:aria-label可访问性标注属于落地时的细节加强。表单层SelfSignedCertFields 的统一改造替换 Domains / IP Addresses 编辑器SelfSignedCertFields.vue 中原先的ASelect modetags被替换为AFormItem :label$gettext(Domains) StringListInput v-modeldata.domains :placeholder$gettext(Enter domain name) :add-button-text$gettext(Add Domain) / /AFormItem AFormItem :label$gettext(IP Addresses) StringListInput v-modeldata.ip_addresses :placeholder$gettext(Enter IP address) :add-button-text$gettext(Add IP Address) / /AFormItemv-model直接绑定到defineModelSelfSignedCertPayload派生出的data与原先的 tags 下拉保持同样的数据契约但交互从「chip 输入」变成「多行可增删输入」与 Custom Domains 分支完全一致。自动续期提示与 hideRenewalNote 开关表单顶部新增AAlert提示由新 prophideRenewalNote?: boolean默认false控制显隐AAlert v-if!props.hideRenewalNote classmb-4 typeinfo show-icon :title$gettext(Nginx UI will automatically renew this certificate as it approaches expiration, based on the global certificate renewal interval and this certificate\s validity period.) /消息文案明确定位了续期策略的两个决定因素全局证书续期间隔与本张证书的有效期。同时Name 字段的AFormItem增加required属性显示红色星号placeholder 从原来的Optional改为Enter certificate name。避免双重提示SelfSignedCertManagement 的接线SelfSignedCertManagement.vue 是「编辑已存在自签名证书」的面板它本身已经有一条typesuccess的提示「This self-signed certificate is managed by Nginx UI and renewed automatically.」。若再叠加SelfSignedCertFields的 info 提示用户会看到两条相邻且语义重复的横幅。因此该处显式传hide-renewal-note关闭新提示SelfSignedCertFields v-modeldata is-key-type-readonly hide-renewal-note /这形成了清晰的提示策略新建场景显示续期策略说明编辑场景由管理面板自身的成功态提示负责。数据流Payload 种子与提交过滤模式StringListInput会在模型数组中保留一个空占位行因此三条写入路径需要做到「一进一出」的对称处理进入编辑态seed数组为空时先填充[]保证编辑器渲染出一个可输入的空行提交前filtertrim()去空白、filter(Boolean)丢弃空串再发送给 API。三个种子工厂计划文档明确要求三个 payload 工厂统一种子策略仓库代码已实现DNSIssueCertificate.vue 的emptySelfSignedPayload()→domains: []、ip_addresses: []SelfSignedCertForm.vue 的emptyForm()→domains: defaultDomains?.length ? [...defaultDomains] : []、ip_addresses: []若外部传入defaultDomains则原样继承否则也种子空行cert.ts 的toSelfSignedPayload(c)→domains: c.domains?.length ? [...c.domains] : []、ip_addresses: c.self_signed_config?.ip_addresses?.length ? [...c.self_signed_config.ip_addresses] : []编辑已有证书时按存量数据决定。统一的提交前校验形状三条提交/保存路径使用完全一致的「裁剪 双重校验」模板const name (payload.name ?? ).trim() const domains payload.domains.map(d d.trim()).filter(Boolean) const ip_addresses payload.ip_addresses.map(s s.trim()).filter(Boolean) if (!name) { message.error($gettext(Please enter a name for the certificate)) return } if (domains.length 0 ip_addresses.length 0) { message.error($gettext(Please enter at least one domain or IP address)) return } await cert.generate_self_signed({ ...payload, name, domains, ip_addresses })应用此模式的三处位置触发时机调用的 APIDNSIssueCertificate.submitSelfSigned()申请证书对话框 → Self-signed → Generatecert.generate_self_signedSelfSignedCertForm.submit()站点编辑器内的自签名快捷创建cert.generate_self_signedCertificateEditor.save()isSelfSigned分支编辑已有自签名证书后保存cert.modify_self_signed校验顺序值得注意先校验 Name再校验域名/IP 至少填一项——这是刻意设计的消息优先级保证用户先收到关于必填 Name 的明确指引。对应 API 封装在 cert.ts 中generate_self_signedPOST 到/self_signed_certmodify_self_signedPOST 到/self_signed_cert/{id}。后端契约binding:required 与接口测试请求结构变更后端校验统一收敛在请求边界。api/certificate/self_signed.go 中SelfSignedCertRequest的完整结构如下Name已带binding:requiredtype SelfSignedCertRequest struct { Name string json:name binding:required Domains []string json:domains binding:omitempty IPAddresses []string json:ip_addresses binding:omitempty,dive,ip KeyType string json:key_type binding:omitempty,auto_cert_key_type ValidityDays int json:validity_days binding:omitempty,min1,max3650 SyncNodeIds []uint64 json:sync_node_ids binding:omitempty }binding:required通过cosy.BindAndValid在GenerateSelfSignedCert与ModifySelfSignedCert两个 handler 中同时生效两者绑定同一个结构体空 Name 会在进入业务逻辑前被拦截。其余字段维持既有约束IP 逐项dive,ip校验、KeyType 走自定义校验器auto_cert_key_type、有效期限制在 1–3650 天。防御性代码保留计划与设计文档都强调GenerateSelfSignedCert中selfSignedSlug的 CommonName 回退逻辑self_signed.go 中 slug 为空时回退到defaultSelfSignedSlug或首个域名/IP保留为防御性代码——虽然对新请求而言空 Name 已被拦截但直接调用selfSignedSlug的内部路径仍受保护。配套的 slug 测试如TestSelfSignedSlugConvertsIDNToPunycode验证中文域名转 punycode、TestSelfSignedSlugSanitizesPathTraversal验证路径穿越清理佐证了这一设计意图。新增接口测试api/certificate/self_signed_test.go 中已落地TestGenerateSelfSignedCertRejectsEmptyName它复用了与 rollback 测试相同的setupSelfSignedAPITest夹具gin TestMode 内存 sqlite query.SetDefault核心断言是body, err : json.Marshal(SelfSignedCertRequest{ Domains: []string{named.example}, KeyType: string(certcrypto.EC256), ValidityDays: 30, }) // POST /self_signed_cert ... if rec.Code 400 || rec.Code 500 { t.Fatalf(status %d, want a 4xx for missing name, rec.Code) } if !strings.Contains(strings.ToLower(rec.Body.String()), name) { t.Fatalf(response body %q did not mention the missing name field, rec.Body.String()) }该测试不要求 4xx 的精确状态码但断言响应体中必须提及name字段确保错误信息对用户可诊断。既有测试TestGenerateSelfSignedCertRollsBackDBOnFileWriteFailure发送的请求自带Name: rollback-test不受新约束影响buildSelfSignedOptions的单元测试走内部函数、不经过 binding 校验同样不受影响——这保证了新增校验不会破坏存量测试。验证体系三层质量门计划文档将验证拆成三层值得作为同类改造的范式参考。前端质量门cd app bun run lint bun run typecheck两命令都必须退出码 0。若perfectionist规则重排了 import 顺序项目 lint 配置的已知行为运行bun run lint:fix后复检即可。计划明确不为该视图新增组件级单测项目此视图没有组件测试先例行为由手动冒烟覆盖。后端质量门go test ./api/certificate/ -run TestGenerateSelfSignedCertRejectsEmptyName -race # 新测试 go test ./api/certificate/ ./internal/cert/ -race # 全量回归格式化方面gofmt是硬性门槛goimports若未安装可忽略。设计文档的完整回归范围是go test ./api/certificate/... ./internal/cert/...。手动冒烟清单计划文档为人工验收列出了 8 项检查覆盖三条入口申请证书对话框、证书编辑器、站点编辑器快捷入口与两种状态新建、编辑申请证书 → Self-signed顶部可见续期提示Name 字段带红色星号、placeholder 为Enter certificate nameDomains / IP Addresses 各渲染为「空行 Add 按钮」的多行编辑器全空点 Generate → toastPlease enter a name for the certificate只填 Name → toastPlease enter at least one domain or IP address填 Name 一个域名 → 证书创建成功、对话框关闭、列表刷新且显示名称打开已有自签名证书 → 仅显示管理面板自身的续期提示不出现第二个提示hide-renewal-note生效编辑已有证书时清空 Name → 保存被拦截直到 Name 补齐站点编辑器的自签名快捷入口 → 续期提示可见、Name 必填Custom Domains 分支 → 外观与行为与改造前一致视觉回归检查。i18n 新增字符串设计文档列出了本次新增的英文源串$gettext提取自 .vue 模板源语言为英文其他语言的 .po 文件在仓库 app/src/language 下同步维护Add Item组件默认按钮文案Add Domain沿用已有Add IP AddressEnter domain name沿用已有Enter IP addressEnter certificate namePlease enter a name for the certificateNginx UI will automatically renew this certificate as it approaches expiration, based on the global certificate renewal interval and this certificates validity period.messages.pot的重新生成属于独立的运维跟进项不在本次改造范围内。落地节奏任务分解与提交规范实现计划将整个改造拆为 6 个代码提交 1 个收尾任务顺序本身包含工程意图——先完成后端契约变更Task 2使后续前端改动始终对着新服务端契约做端到端验证任务内容产物Task 1创建StringListInput组件 re-export新增 2 文件Task 2后端binding:required 先写失败测试改self_signed.go/self_signed_test.goTask 3SelfSignedCertFields接入组件、加续期提示、Name 必填改 1 文件Task 4SelfSignedCertManagement传hide-renewal-note改 1 文件Task 5三个提交/保存路径种子 过滤 校验 Name改 4 文件Task 6Custom Domains 切换到StringListInput纯重构改 1 文件Task 7最终 lint/typecheck/go test 全量 手动冒烟交接无代码产物提交信息遵循项目的 imperative 风格并要求 sign-offCo-Authored-By开发分支为feature/self-signed-certificate计划要求直接在该分支提交、不另开子分支。Task 6 被刻意标记为「纯重构同一 UX、更少重复」放在最后单独提交便于 review 时与行为变更隔离。小结一处抽象、三处接线、两层校验本次增强的精髓可以浓缩为「一处抽象、三处接线、两层校验」一个StringListInput组件统一了 Custom Domains 与自签名 Domains / IP Addresses 的编辑模型三个 payload 工厂对称处理空行种子、三条提交路径统一裁剪过滤Name 必填在前端toast 提示与后端binding:required 接口测试双重落地。改造还通过hideRenewalNote精确控制了提示信息的展示语境避免信息噪音。无论是后续扩展新的多行字符串输入场景还是为其他表单补充请求边界校验这套「组件抽象 种子/过滤对称处理 边界契约测试」的模式都具备直接的可复用价值。赞分享后端前端运维MCP 服务【免费下载链接】nginx-uiYet another WebUI for Nginx项目地址https://gitcode.com/gh_mirrors/ngi/nginx-ui点击查看免费下载相关推荐Nginx Proxy Manager 证书全解析HTTP、DNS 与自定义证书的签发、验证与续期指南Nginx Proxy Manager 证书全解析HTTP、DNS 与自定义证书的签发、验证与续期指南 Nginx Proxy ManagerNPM为托管后端API网关nginx-proxy-manager 证书管理实战HTTP、DNS 与自定义证书的签发与自动续期nginx proxy manager 证书管理实战HTTP、DNS 与自定义证书的签发与自动续期 导读 本文围绕 nginx proxy manager 前后端API网关Sunshine 游戏串流实战指南30 分钟上手客厅大屏畅玩 PC 游戏Sunshine 游戏串流实战指南30 分钟上手客厅大屏畅玩 PC 游戏 游戏 PC 摆在书房你却在客厅想玩。给这台 PC 装上 Sunshine 就行。音视频后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表