From 78ae5cb17b4504e8d27b9289c70828ea5e55223b Mon Sep 17 00:00:00 2001 From: smail Date: Sun, 30 Aug 2026 20:26:19 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=B7=B3=E8=BD=AC=E4=B8=8E=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E9=80=80=E5=87=BA=E7=AB=9E=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fe/src/views/Setup/Setup.js | 212 +++++++++++++--------- server/controllers/setup.go | 44 ++++- server/controllers/setup_contract_test.go | 40 ++++ server/signal/signal.go | 2 +- 4 files changed, 202 insertions(+), 96 deletions(-) diff --git a/fe/src/views/Setup/Setup.js b/fe/src/views/Setup/Setup.js index f7fccd11..4159b88c 100644 --- a/fe/src/views/Setup/Setup.js +++ b/fe/src/views/Setup/Setup.js @@ -19,6 +19,27 @@ const setupRequest = (payload) => http.post("/api/v1/setup", payload).then((resp return response }) +const getSetupErrorMessage = (error, fallback = lang.setup_failed) => { + const message = typeof error?.message === "string" ? error.message.trim() : "" + if (message && message !== "Network Error" && !/^timeout of \d+ms exceeded$/i.test(message)) { + return message + } + return fallback +} + +const showSetupError = (error, fallback = lang.setup_failed) => { + const message = getSetupErrorMessage(error, fallback) + ElMessage.error(message) + return message +} + +const assertSetupSuccess = (response, fallback = lang.setup_failed) => { + if (response.errorNo !== 0) { + throw new Error(response.errorMsg || fallback) + } + return response +} + export function usePage() { const waitDesc = ref(lang.wait_desc); @@ -79,7 +100,7 @@ export function usePage() { )) const port = ref(80) - +`r`n const sslRuntime = ref({})`r`n const addDomain = () => { domainSettings.multi_domain.push([]) } @@ -109,28 +130,22 @@ export function usePage() { return } - setupRequest( { + setupRequest({ "action": "set", "step": "password", "account": adminSettings.account, "password": adminSettings.password }).then((res) => { - if (res.errorNo !== 0) { - ElMessage.error(res.errorMsg || lang.setup_failed) - } else { - active.value++; - getDomainConfig(); - } - }).catch(() => { - ElMessage.error(lang.setup_failed) + assertSetupSuccess(res) + active.value++ + return getDomainConfig() + }).catch((error) => { + showSetupError(error) }) } - const getPassword = () => setupRequest( {"action": "get", "step": "password"}).then((res) => { - if (res.errorNo !== 0) { - throw new Error(res.errorMsg || lang.db_init_failed) - } - + const getPassword = () => setupRequest({"action": "get", "step": "password"}).then((res) => { + assertSetupSuccess(res, lang.db_init_failed) adminSettings.hadSeted = res.data != "" if (adminSettings.hadSeted) { adminSettings.account = res.data @@ -140,10 +155,8 @@ export function usePage() { }) const getDbConfig = () => { - return setupRequest( {"action": "get", "step": "database"}).then((res) => { - if (res.errorNo !== 0) { - throw new Error(res.errorMsg || lang.db_request_failed) - } + return setupRequest({"action": "get", "step": "database"}).then((res) => { + assertSetupSuccess(res, lang.db_request_failed) dbSettings.type = res.data.db_type; dbSettings.mode = res.data.db_mode || "dsn"; dbSettings.dsn = res.data.db_dsn || ""; @@ -159,14 +172,15 @@ export function usePage() { } const getDomainConfig = () => { - setupRequest( {"action": "get", "step": "domain"}).then((res) => { - if (res.errorNo !== 0) { - ElMessage.error(res.errorMsg) - } else { - domainSettings.web_domain = res.data.web_domain; - domainSettings.smtp_domain = res.data.smtp_domain; - domainSettings.multi_domain = res.data.domains; - } + return setupRequest({"action": "get", "step": "domain"}).then((res) => { + assertSetupSuccess(res) + domainSettings.web_domain = res.data.web_domain; + domainSettings.smtp_domain = res.data.smtp_domain; + domainSettings.multi_domain = res.data.domains; + return res + }).catch((error) => { + showSetupError(error) + return null }) } @@ -221,12 +235,8 @@ export function usePage() { "db_name": dbSettings.name } - return setupRequest( payload).then((res) => { - if (res.errorNo !== 0) { - databaseError.value = res.errorMsg || lang.db_init_failed - ElMessage.error(databaseError.value) - return - } + return setupRequest(payload).then((res) => { + assertSetupSuccess(res, lang.db_init_failed) return getPassword().then(() => { ElMessage.success(lang.db_init_success) active.value++ @@ -261,12 +271,13 @@ export function usePage() { } const getDNSConfig = () => { - setupRequest( {"action": "get", "step": "dns"}).then((res) => { - if (res.errorNo !== 0) { - ElMessage.error(res.errorMsg) - } else { - dnsInfos.value = res.data - } + return setupRequest({"action": "get", "step": "dns"}).then((res) => { + assertSetupSuccess(res) + dnsInfos.value = res.data + return res + }).catch((error) => { + showSetupError(error) + return null }) } @@ -306,19 +317,18 @@ export function usePage() { } const getSSLConfig = () => { - setupRequest( {"action": "get", "step": "ssl"}).then((res) => { - if (res.errorNo !== 0) { - ElMessage.error(res.errorMsg) - } else { - sslSettings.type = res.data.type - if (sslSettings.type === "2") { - sslSettings.type = "0" - sslSettings.challenge = "dns" - } - - - port.value = res.data.port + return setupRequest({"action": "get", "step": "ssl"}).then((res) => { + assertSetupSuccess(res) + sslSettings.type = res.data.type + if (sslSettings.type === "2") { + sslSettings.type = "0" + sslSettings.challenge = "dns" } + sslRuntime.value = res.data`r`n port.value = res.data.port || res.data.httpPort || 80 + return res + }).catch((error) => { + showSetupError(error) + return null }) } @@ -331,36 +341,63 @@ export function usePage() { } - setupRequest( { + setupRequest({ "action": "set", "step": "ssl", "ssl_type": sslType, "key_path": sslSettings.key_path, "crt_path": sslSettings.crt_path }).then((res) => { - if (res.errorNo !== 0) { - fullscreenLoading.value = false; - ElMessage.error(res.errorMsg) - } else { - if (sslType === "2") { - fullscreenLoading.value = false; - dnsChecking.value = true; - getSSLDNSParams(); - return - } - checkStatus(); + assertSetupSuccess(res) + if (sslType === "2") { + fullscreenLoading.value = false + dnsChecking.value = true + getSSLDNSParams() + return } + if (sslType === "1") {`r`n window.location.href = buildFinalURL(sslRuntime.value)`r`n return`r`n }`r`n checkStatus() + }).catch((error) => { + fullscreenLoading.value = false + showSetupError(error) }) } + + const buildFinalURL = (sslData) => { + const scheme = "https:" + const configuredDomain = String(domainSettings.web_domain || "").trim() + if (!configuredDomain) { + return window.location.href + } + + let target + try { + target = new URL(`${scheme}//${configuredDomain}`) + } catch (error) { + return window.location.href + } + + const currentHost = window.location.hostname.toLowerCase() + const targetHost = target.hostname.toLowerCase() + const currentPort = window.location.port + const isSameHost = currentHost === targetHost + const targetPort = sslSettings.type === "1" ? sslData?.httpPort : sslData?.httpsPort + const proxyMode = sslData?.tlsMode === "proxy" + if (isSameHost && currentPort && !proxyMode) { + target.port = targetPort > 0 ? String(targetPort) : currentPort + } else if (targetPort > 0 && ((scheme === "http:" && targetPort !== 80) || (scheme === "https:" && targetPort !== 443))) { + target.port = String(targetPort) + } + + target.pathname = window.location.pathname + target.search = window.location.search + target.hash = window.location.hash + return target.toString() + } const checkStatus = () => { - setupRequest( {"action": "get", "step": "ssl"}).then((res) => { + setupRequest({"action": "get", "step": "ssl"}).then((res) => { if (res.errorNo === 0 && res.data.ready) { - if (sslSettings.type === "1") { - window.location.href = "http://" + domainSettings.web_domain; - } else { - window.location.href = "https://" + domainSettings.web_domain; - } + window.location.href = buildFinalURL(res.data) return } setTimeout(checkStatus, 1000) @@ -370,32 +407,32 @@ export function usePage() { } const setDomainConfig = () => { - setupRequest( { + setupRequest({ "action": "set", "step": "domain", "web_domain": domainSettings.web_domain, "smtp_domain": domainSettings.smtp_domain, "multi_domain": domainSettings.multi_domain.join(",") }).then((res) => { - if (res.errorNo !== 0) { - ElMessage.error(res.errorMsg) - } else { - active.value++; - getDNSConfig(); - } + assertSetupSuccess(res) + active.value++ + return getDNSConfig() + }).catch((error) => { + showSetupError(error) }) } const getSSLDNSParams = () => { - setupRequest( {"action": "getParams", "step": "ssl"}).then((res) => { - if (res.errorNo !== 0) { - ElMessage.error(res.errorMsg) - } else { - sslSettings.paramsList = Array.isArray(res.data) ? res.data : [] - if (dnsChecking.value && sslSettings.paramsList.length === 0) { - setTimeout(getSSLDNSParams, 1000) - } + return setupRequest({"action": "getParams", "step": "ssl"}).then((res) => { + assertSetupSuccess(res) + sslSettings.paramsList = Array.isArray(res.data) ? res.data : [] + if (dnsChecking.value && sslSettings.paramsList.length === 0) { + setTimeout(getSSLDNSParams, 1000) } + return res + }).catch((error) => { + showSetupError(error) + return null }) } @@ -415,8 +452,11 @@ export function usePage() { setDomainConfig(); break; case 4: - getSSLConfig(); - active.value++ + getSSLConfig().then((response) => { + if (response) { + active.value++ + } + }) break case 5: if (dnsChecking.value) { diff --git a/server/controllers/setup.go b/server/controllers/setup.go index 37475f14..03251dc8 100644 --- a/server/controllers/setup.go +++ b/server/controllers/setup.go @@ -40,9 +40,40 @@ func AcmeChallenge(w http.ResponseWriter, r *http.Request) { } type sslResponse struct { - Port int `json:"port"` - Type string `json:"type"` - Ready bool `json:"ready"` + Port int `json:"port"` + HTTPPort int `json:"httpPort"` + HTTPSPort int `json:"httpsPort"` + TLSMode string `json:"tlsMode"` + HTTPSEnabled bool `json:"httpsEnabled"` + WebDomain string `json:"webDomain"` + Type string `json:"type"` + Ready bool `json:"ready"` +} + +func buildSetupSSLResponse(sslType string, cfg *config.Config, httpConfig config.HTTPConfig, webDomain string, ready bool) sslResponse { + httpPort := httpConfig.Port + if httpPort <= 0 && cfg != nil { + httpPort = cfg.GetSetupPort() + } + httpsPort := httpConfig.HTTPSPort + if httpsPort <= 0 { + httpsPort = 443 + } + return sslResponse{ + Port: httpPort, + HTTPPort: httpPort, + HTTPSPort: httpsPort, + TLSMode: httpConfig.TLSMode, + HTTPSEnabled: httpConfig.HTTPSEnabled, + WebDomain: webDomain, + Type: sslType, + Ready: ready, + } +} + +func currentSetupSSLResponse(sslType string) sslResponse { + cfg := config.CurrentConfig() + return buildSetupSSLResponse(sslType, cfg, config.CurrentHTTPConfig(), config.CurrentWebDomain(), config.IsSetupReady()) } func writeSetupError(ctx *context.Context, w http.ResponseWriter, err error) { @@ -163,12 +194,7 @@ func Setup(ctx *context.Context, w http.ResponseWriter, req *http.Request) { writeSetupError(ctx, w, err) return } - res := sslResponse{ - Type: sslType, - Port: config.CurrentConfig().GetSetupPort(), - Ready: config.IsSetupReady(), - } - response.NewSuccessResponse(res).FPrint(w) + response.NewSuccessResponse(currentSetupSSLResponse(sslType)).FPrint(w) return } diff --git a/server/controllers/setup_contract_test.go b/server/controllers/setup_contract_test.go index 9b7c72d9..c9455983 100644 --- a/server/controllers/setup_contract_test.go +++ b/server/controllers/setup_contract_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "git.pcux.cn/xuans/Smail/config" appcontext "git.pcux.cn/xuans/Smail/utils/context" ) @@ -32,3 +33,42 @@ func TestSetupReturnsJSONForUnsupportedOperation(t *testing.T) { t.Fatalf("unexpected setup response: %+v", payload) } } + +func TestBuildSetupSSLResponseUsesConfiguredProxyPorts(t *testing.T) { + cfg := &config.Config{HttpPort: 9001} + response := buildSetupSSLResponse( + config.SSLTypeUser, + cfg, + config.HTTPConfig{ + Port: 9001, + HTTPSPort: 9002, + TLSMode: config.HTTPSTLSModeProxy, + HTTPSEnabled: false, + }, + "xuans.com", + true, + ) + + if response.HTTPPort != 9001 || response.HTTPSPort != 9002 { + t.Fatalf("unexpected configured ports: http=%d https=%d", response.HTTPPort, response.HTTPSPort) + } + if response.TLSMode != config.HTTPSTLSModeProxy || response.HTTPSEnabled { + t.Fatalf("unexpected proxy settings: mode=%q enabled=%v", response.TLSMode, response.HTTPSEnabled) + } + if response.WebDomain != "xuans.com" || !response.Ready { + t.Fatalf("unexpected setup response identity: domain=%q ready=%v", response.WebDomain, response.Ready) + } +} + +func TestBuildSetupSSLResponseDefaultsHTTPSPort(t *testing.T) { + response := buildSetupSSLResponse( + config.SSLTypeAutoHTTP, + &config.Config{HttpPort: 9001}, + config.HTTPConfig{Port: 9001}, + "mail.example.com", + false, + ) + if response.HTTPPort != 9001 || response.HTTPSPort != 443 { + t.Fatalf("unexpected default ports: http=%d https=%d", response.HTTPPort, response.HTTPSPort) + } +} diff --git a/server/signal/signal.go b/server/signal/signal.go index a37a06cf..127fefa3 100644 --- a/server/signal/signal.go +++ b/server/signal/signal.go @@ -1,7 +1,7 @@ package signal // InitChan 控制初始化流程结束 -var InitChan = make(chan bool) +var InitChan = make(chan bool, 1) // RestartChan 控制程序重启 var RestartChan = make(chan bool)