mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2026-06-19 07:36:59 +00:00
chore(cert): fix small bugs with review
- shortError now truncates by rune count instead of bytes, so non-ASCII error messages (e.g. localized ACME / DNS provider errors) cannot be split mid-rune. TestShortError gains a CJK case asserting valid UTF-8. - Cert.last_attempt_at is typed string | null on the frontend to reflect that the *time.Time pointer serializes as null for legacy / pre-attempt rows. - Drop redundant ?. on refModal / refObtainCertLive in the three click handlers. The refs are bound to components rendered alongside their trigger button, so they are guaranteed to be mounted by the time the handler fires.
This commit is contained in:
@@ -226,14 +226,17 @@ func markCertSuccess(id uint64, sslCertificatePath, sslCertificateKeyPath string
|
||||
|
||||
// shortError trims and truncates an error for UI display in last_error.
|
||||
// Returns "" for nil so a successful retry can clear the prior error.
|
||||
// Truncation is rune-aware so non-ASCII error messages (e.g. localized
|
||||
// ACME or DNS provider errors) cannot be split mid-rune.
|
||||
func shortError(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
msg := strings.TrimSpace(err.Error())
|
||||
const max = 500
|
||||
if len(msg) > max {
|
||||
msg = msg[:max] + "…"
|
||||
const maxRunes = 500
|
||||
runes := []rune(msg)
|
||||
if len(runes) > maxRunes {
|
||||
msg = string(runes[:maxRunes]) + "…"
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/0xJacky/Nginx-UI/internal/cert"
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
@@ -142,13 +144,25 @@ func TestMarkCertSuccessClearsLastError(t *testing.T) {
|
||||
func TestShortError(t *testing.T) {
|
||||
assert.Equal(t, "", shortError(nil))
|
||||
assert.Equal(t, "hello", shortError(errString(" hello ")))
|
||||
|
||||
long := make([]byte, 600)
|
||||
for i := range long {
|
||||
long[i] = 'a'
|
||||
}
|
||||
got := shortError(errString(string(long)))
|
||||
// 500 ASCII runes + the literal "…" suffix.
|
||||
assert.Equal(t, 500+len("…"), len(got))
|
||||
assert.Equal(t, "…", got[len(got)-len("…"):])
|
||||
|
||||
// Multi-byte runes must not be split: each CJK char is 3 bytes,
|
||||
// 600 chars = 1800 bytes, which would corrupt the boundary if we
|
||||
// sliced by bytes. After rune-aware truncation we expect exactly
|
||||
// 500 CJK chars + the "…" suffix, and the result must be valid UTF-8.
|
||||
multi := strings.Repeat("中", 600)
|
||||
gotMulti := shortError(errString(multi))
|
||||
assert.True(t, utf8.ValidString(gotMulti), "truncated message must be valid UTF-8")
|
||||
assert.Equal(t, 500+1 /* ellipsis rune */, utf8.RuneCountInString(gotMulti))
|
||||
assert.Equal(t, "…", gotMulti[len(gotMulti)-len("…"):])
|
||||
}
|
||||
|
||||
type errString string
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ export interface Cert extends ModelBase {
|
||||
revoke_old: boolean
|
||||
status: CertStatusType
|
||||
last_error: string
|
||||
last_attempt_at: string
|
||||
last_attempt_at: string | null
|
||||
}
|
||||
|
||||
export interface CertificateInfo {
|
||||
|
||||
@@ -90,8 +90,10 @@ function issueCert() {
|
||||
step.value = 1
|
||||
modalVisible.value = true
|
||||
|
||||
refObtainCertLive.value
|
||||
?.issue_cert(computedMainDomain.value, computedDomains.value, data.value.key_type)
|
||||
// ObtainCertLive is mounted in the same modal via force-render, so the
|
||||
// ref is guaranteed to be available by the time this function runs.
|
||||
refObtainCertLive.value!
|
||||
.issue_cert(computedMainDomain.value, computedDomains.value, data.value.key_type)
|
||||
.then(() => {
|
||||
message.success($gettext('Issued successfully'))
|
||||
emit('issued')
|
||||
|
||||
@@ -20,7 +20,9 @@ async function issueCert() {
|
||||
await certStore.save()
|
||||
message.success($gettext('Save successfully'))
|
||||
|
||||
refModal.value?.start().then(() => {
|
||||
// refModal is mounted alongside this button via force-render, so it
|
||||
// is guaranteed to be available by the time @click fires.
|
||||
refModal.value!.start().then(() => {
|
||||
message.success($gettext('Renew successfully'))
|
||||
emit('renewed')
|
||||
})
|
||||
|
||||
@@ -25,8 +25,10 @@ const issueOptions = computed<AutoCertOptions>(() => ({
|
||||
}))
|
||||
|
||||
function openAndRetry() {
|
||||
refModal.value
|
||||
?.start()
|
||||
// refModal is bound to a component rendered alongside this button,
|
||||
// so it is guaranteed to be mounted by the time @click fires.
|
||||
refModal.value!
|
||||
.start()
|
||||
.then(() => {
|
||||
message.success($gettext('Certificate issued successfully'))
|
||||
emit('retried')
|
||||
|
||||
Reference in New Issue
Block a user