mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-11-03 16:05:26 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bc95c270f |
@@ -666,15 +666,59 @@ func (s *MCPWebsiteBindingService) matchesInitialSite(req request.MCPWebsiteBind
|
||||
if err != nil || (site.Protocol == constant.ProtocolHTTPS) != req.EnableSSL || site.WebsiteSSLID != req.SSLID {
|
||||
return false
|
||||
}
|
||||
if port == 0 {
|
||||
explicitPort := port != 0
|
||||
if !explicitPort || req.EnableSSL {
|
||||
installed, installErr := s.openresty()
|
||||
if installErr != nil {
|
||||
return false
|
||||
}
|
||||
port = installed.HttpPort
|
||||
if !explicitPort {
|
||||
port = installed.HttpPort
|
||||
}
|
||||
if req.EnableSSL && port == installed.HttpPort {
|
||||
for _, domain := range site.Domains {
|
||||
if domain.Domain == host && domain.Port == installed.HttpPort && !domain.SSL {
|
||||
return true // Complete an interrupted bind against its existing HTTP base.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, domain := range site.Domains {
|
||||
if domain.Domain == host && domain.Port == port && !domain.SSL {
|
||||
if domain.Domain == host && domain.Port == port && domain.SSL == (req.EnableSSL && explicitPort) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func mcpWebsiteListenConflict(candidate model.Website, installed model.AppInstall) error {
|
||||
if err := checkWebsiteListenConflict(candidate, installed.HttpPort, installed.HttpsPort); err != nil {
|
||||
return err
|
||||
}
|
||||
sites, err := websiteRepo.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ports := websiteListenPorts(candidate, installed.HttpPort, installed.HttpsPort)
|
||||
isTLS := func(site model.Website, port int) bool {
|
||||
return site.Protocol == constant.ProtocolHTTPS && slices.Contains(buildWebsiteTLSPlan(site.Domains, installed.HttpPort, installed.HttpsPort).httpsPorts, port)
|
||||
}
|
||||
for _, site := range sites {
|
||||
if site.ID == candidate.ID || site.Type == constant.Stream {
|
||||
continue
|
||||
}
|
||||
for port := range websiteListenPorts(site, installed.HttpPort, installed.HttpsPort) {
|
||||
if _, exists := ports[port]; exists && isTLS(candidate, port) != isTLS(site, port) {
|
||||
return buserr.WithName("ErrDomainIsUsed", site.PrimaryDomain)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func mcpWebsiteTLSUsesHTTPPort(site model.Website, installed model.AppInstall) bool {
|
||||
for _, domain := range site.Domains {
|
||||
if domain.SSL && domain.Port == installed.HttpPort {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -835,6 +879,14 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
if req.DeleteWebsite {
|
||||
return s.missingDeletedWebsite(req)
|
||||
}
|
||||
if req.Operation == "status" && req.WebsiteID == 0 {
|
||||
partial := model.Website{Alias: mcpWebsiteAlias(req.BindingID)}
|
||||
for _, location := range []string{GetSitePath(partial, SiteDir), GetSitePath(partial, SiteConf), mcpWebsitePrivatePath(partial, mcpWebsiteOwnerName), mcpWebsitePrivatePath(partial, mcpWebsiteProxyName)} {
|
||||
if _, err := os.Lstat(location); !os.IsNotExist(err) {
|
||||
return response.MCPWebsiteBinding{State: "unavailable", BindingID: req.BindingID, RequestID: req.RequestID, Reason: "website_missing"}
|
||||
}
|
||||
}
|
||||
}
|
||||
if req.WebsiteID == 0 || req.Operation == "unbind" {
|
||||
return response.MCPWebsiteBinding{State: "unbound", BindingID: req.BindingID, RequestID: req.RequestID}
|
||||
}
|
||||
@@ -843,6 +895,12 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
if req.Operation != "bind" {
|
||||
return mcpWebsiteFailure("website_missing", true, true)
|
||||
}
|
||||
if req.WebsiteID != 0 {
|
||||
return mcpWebsiteFailure("website_missing", true, true)
|
||||
}
|
||||
if _, ownerErr := os.Lstat(mcpWebsitePrivatePath(model.Website{Alias: mcpWebsiteAlias(req.BindingID)}, mcpWebsiteOwnerName)); !os.IsNotExist(ownerErr) {
|
||||
return mcpWebsiteFailure("website_ownership_changed", false, true)
|
||||
}
|
||||
installed, installErr := s.openresty()
|
||||
if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning {
|
||||
return mcpWebsiteFailure("openresty_unavailable", true, false)
|
||||
@@ -852,17 +910,19 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
return mcpWebsiteFailure("website_group_unavailable", true, false)
|
||||
}
|
||||
domain, port, _ := mcpWebsiteDomain(req.Domain)
|
||||
explicitPort := port != 0
|
||||
if port == 0 {
|
||||
port = installed.HttpPort
|
||||
}
|
||||
if req.EnableSSL && port != installed.HttpPort {
|
||||
return mcpWebsiteFailure("http_port_requires_default", true, false)
|
||||
}
|
||||
candidate := model.Website{Protocol: constant.ProtocolHTTP, Domains: []model.WebsiteDomain{{Domain: domain, Port: port}}}
|
||||
domainSSL := req.EnableSSL && explicitPort
|
||||
candidate := model.Website{Protocol: constant.ProtocolHTTP, Domains: []model.WebsiteDomain{{Domain: domain, Port: port, SSL: domainSSL}}}
|
||||
if req.EnableSSL {
|
||||
candidate.Protocol = constant.ProtocolHTTPS
|
||||
if mcpWebsiteTLSUsesHTTPPort(candidate, installed) {
|
||||
return mcpWebsiteFailure("invalid_https_port", true, false)
|
||||
}
|
||||
}
|
||||
if conflictErr := checkWebsiteListenConflict(candidate, installed.HttpPort, installed.HttpsPort); conflictErr != nil {
|
||||
if conflictErr := mcpWebsiteListenConflict(candidate, installed); conflictErr != nil {
|
||||
var business buserr.BusinessError
|
||||
if errors.As(conflictErr, &business) && business.Msg == "ErrDomainIsUsed" {
|
||||
return mcpWebsiteFailure("domain_conflict", true, true)
|
||||
@@ -870,7 +930,7 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
return mcpWebsiteFailure("website_unavailable", false, false)
|
||||
}
|
||||
create := request.WebsiteCreate{Alias: mcpWebsiteAlias(req.BindingID), Type: constant.Static, Remark: mcpWebsiteRemark(req), WebsiteGroupID: group.ID,
|
||||
Domains: []request.WebsiteDomain{{Domain: domain, Port: port}}, SSLConfig: request.SSLConfig{EnableSSL: req.EnableSSL, WebsiteSSLID: req.SSLID}}
|
||||
Domains: []request.WebsiteDomain{{Domain: domain, Port: port, SSL: domainSSL}}, SSLConfig: request.SSLConfig{EnableSSL: req.EnableSSL, WebsiteSSLID: req.SSLID}}
|
||||
previousMutation := mutationStarted
|
||||
mutationStarted = true
|
||||
if createErr := s.createWebsite(create); createErr != nil {
|
||||
@@ -966,6 +1026,27 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
if req.Operation == "bind" && owner.BindHash != mcpWebsiteBindHash(req) {
|
||||
return mcpWebsiteFailure("binding_request_changed", true, true)
|
||||
}
|
||||
if req.Operation == "update" && (site.Protocol == constant.ProtocolHTTPS) != req.EnableSSL {
|
||||
installed, installErr := s.openresty()
|
||||
if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning {
|
||||
return mcpWebsiteFailure("openresty_unavailable", true, false)
|
||||
}
|
||||
candidate := site
|
||||
candidate.Protocol = constant.ProtocolHTTP
|
||||
if req.EnableSSL {
|
||||
candidate.Protocol = constant.ProtocolHTTPS
|
||||
if mcpWebsiteTLSUsesHTTPPort(candidate, installed) {
|
||||
return mcpWebsiteFailure("invalid_https_port", true, false)
|
||||
}
|
||||
}
|
||||
if conflictErr := mcpWebsiteListenConflict(candidate, installed); conflictErr != nil {
|
||||
var business buserr.BusinessError
|
||||
if errors.As(conflictErr, &business) && business.Msg == "ErrDomainIsUsed" {
|
||||
return mcpWebsiteFailure("domain_conflict", true, true)
|
||||
}
|
||||
return mcpWebsiteFailure("website_unavailable", false, false)
|
||||
}
|
||||
}
|
||||
if owner.ProxyHash != "" && owner.Operation != "unbind" && req.Operation != "unbind" && !mcpWebsiteProxyIncluded(site) {
|
||||
return mcpWebsiteFailure("proxy_not_loaded", true, true)
|
||||
}
|
||||
@@ -1039,23 +1120,6 @@ func (s *MCPWebsiteBindingService) Handle(ctx context.Context, req request.MCPWe
|
||||
if req.Domain != site.PrimaryDomain {
|
||||
return mcpWebsiteFailure("domain_changed", true, true)
|
||||
}
|
||||
if req.EnableSSL {
|
||||
installed, installErr := s.openresty()
|
||||
if installErr != nil || installed.ID == 0 || installed.Status != constant.StatusRunning {
|
||||
return mcpWebsiteFailure("openresty_unavailable", true, false)
|
||||
}
|
||||
host, _, _ := mcpWebsiteDomain(site.PrimaryDomain)
|
||||
usesHTTPPort := false
|
||||
for _, domain := range site.Domains {
|
||||
if domain.Domain == host && domain.Port == installed.HttpPort && !domain.SSL {
|
||||
usesHTTPPort = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !usesHTTPPort {
|
||||
return mcpWebsiteFailure("http_port_requires_default", true, false)
|
||||
}
|
||||
}
|
||||
if site.Protocol == constant.ProtocolHTTPS && !req.EnableSSL || site.Protocol != constant.ProtocolHTTPS && req.EnableSSL || req.EnableSSL && site.WebsiteSSLID != req.SSLID {
|
||||
mutationStarted = true
|
||||
if err = s.updateHTTPS(context.WithoutCancel(ctx), request.WebsiteHTTPSOp{WebsiteID: site.ID, Enable: req.EnableSSL, Type: constant.SSLExisted, WebsiteSSLID: req.SSLID, HttpConfig: constant.HTTPSOnly}); err != nil {
|
||||
|
||||
@@ -410,6 +410,7 @@ func (s *MCPRuntimeService) mutateWebsiteLocked(ctx context.Context, pending dto
|
||||
(status.WebsiteID == 0 && status.State != "unbound") {
|
||||
return dto.MCPWebsiteStatus{}, mcpWebsiteOperationError("MCP_CONFLICT", "website_state_unconfirmed", false)
|
||||
}
|
||||
row.WebsiteID = status.WebsiteID
|
||||
}
|
||||
if pending.Operation == "bind" {
|
||||
if row.WebsiteID != 0 {
|
||||
|
||||
+31
-18
@@ -23,8 +23,9 @@ const isCsrfForbidden = (response?: AxiosResponse<any>) => {
|
||||
return typeof message === 'string' && message.toLowerCase().includes('csrf token invalid');
|
||||
};
|
||||
|
||||
type RequestConfig = AxiosRequestConfig & {
|
||||
export type RequestConfig = AxiosRequestConfig & {
|
||||
skipErrorMessage?: boolean;
|
||||
errorMessageMode?: 'default' | 'manual';
|
||||
};
|
||||
|
||||
class RequestHttp {
|
||||
@@ -79,7 +80,8 @@ class RequestHttp {
|
||||
async (response: AxiosResponse) => {
|
||||
const globalStore = GlobalStore();
|
||||
const { data } = response;
|
||||
const authResult = handleAuthResponseCode(data, { showRBACMessage: true });
|
||||
const manualErrorMessage = (response.config as RequestConfig)?.errorMessageMode === 'manual';
|
||||
const authResult = handleAuthResponseCode(data, { showRBACMessage: !manualErrorMessage });
|
||||
if (authResult.handled) {
|
||||
if (authResult.action === 'return') {
|
||||
return;
|
||||
@@ -117,12 +119,14 @@ class RequestHttp {
|
||||
return data;
|
||||
}
|
||||
if (data.code && data.code !== ResultEnum.SUCCESS) {
|
||||
if (data.message.toLowerCase().indexOf('operation not permitted') !== -1) {
|
||||
MsgError(i18n.global.t('license.tamperHelper'));
|
||||
return Promise.reject(data);
|
||||
}
|
||||
if (!(response.config as RequestConfig).skipErrorMessage) {
|
||||
MsgError(data.message);
|
||||
if (!manualErrorMessage) {
|
||||
if (data.message.toLowerCase().indexOf('operation not permitted') !== -1) {
|
||||
MsgError(i18n.global.t('license.tamperHelper'));
|
||||
return Promise.reject(data);
|
||||
}
|
||||
if (!(response.config as RequestConfig).skipErrorMessage) {
|
||||
MsgError(data.message);
|
||||
}
|
||||
}
|
||||
return Promise.reject(data);
|
||||
}
|
||||
@@ -130,8 +134,11 @@ class RequestHttp {
|
||||
},
|
||||
async (error: AxiosError) => {
|
||||
const { response } = error;
|
||||
const manualErrorMessage =
|
||||
((error.config ?? response?.config) as RequestConfig | undefined)?.errorMessageMode === 'manual';
|
||||
|
||||
if (error.message.indexOf('timeout') !== -1) MsgError(i18n.global.t('commons.msg.requestTimeout'));
|
||||
if (!manualErrorMessage && error.message.indexOf('timeout') !== -1)
|
||||
MsgError(i18n.global.t('commons.msg.requestTimeout'));
|
||||
if (response) {
|
||||
switch (response.status) {
|
||||
case 313:
|
||||
@@ -141,20 +148,24 @@ class RequestHttp {
|
||||
if (isCsrfForbidden(response)) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
if (response.data && response.data['message']) {
|
||||
MsgError(response.data['message']);
|
||||
} else {
|
||||
MsgError(i18n.global.t('commons.res.forbidden'));
|
||||
if (!manualErrorMessage) {
|
||||
if (response.data && response.data['message']) {
|
||||
MsgError(response.data['message']);
|
||||
} else {
|
||||
MsgError(i18n.global.t('commons.res.forbidden'));
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
case 500:
|
||||
case 502:
|
||||
case 524:
|
||||
case 407:
|
||||
checkStatus(
|
||||
response.status,
|
||||
response.data && response.data['message'] ? response.data['message'] : '',
|
||||
);
|
||||
if (!manualErrorMessage) {
|
||||
checkStatus(
|
||||
response.status,
|
||||
response.data && response.data['message'] ? response.data['message'] : '',
|
||||
);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
default:
|
||||
return Promise.reject(error);
|
||||
@@ -189,12 +200,14 @@ class RequestHttp {
|
||||
...config,
|
||||
});
|
||||
}
|
||||
postLocalNode<T>(url: string, params?: object, timeout?: number): Promise<ResultData<T>> {
|
||||
postLocalNode<T>(url: string, params?: object, timeout?: number, config?: RequestConfig): Promise<ResultData<T>> {
|
||||
return this.service.post(url, params, {
|
||||
baseURL: import.meta.env.VITE_API_URL as string,
|
||||
timeout: timeout ? timeout : (ResultEnum.TIMEOUT as number),
|
||||
withCredentials: true,
|
||||
...config,
|
||||
headers: {
|
||||
...config?.headers,
|
||||
CurrentNode: 'local',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,18 +5,19 @@ import type { Container } from '@/api/interface/container';
|
||||
|
||||
const prefix = '/core/mcp';
|
||||
const local = { headers: { CurrentNode: 'local' } };
|
||||
const website = { ...local, errorMessageMode: 'manual' as const };
|
||||
|
||||
export const getPanelMCPStatus = () => http.get<PanelMCP.ServiceStatus>(`${prefix}/status`, {}, local);
|
||||
export const getPanelMCPDockerStatus = () =>
|
||||
http.get<Container.DockerStatus>('/containers/docker/status', {}, { ...local, timeout: 5000 });
|
||||
export const getPanelMCPSettings = () => http.get<PanelMCP.Settings>(`${prefix}/settings`, {}, local);
|
||||
export const getPanelMCPWebsite = () => http.get<PanelMCP.WebsiteBinding>(`${prefix}/website/status`, {}, local);
|
||||
export const getPanelMCPWebsite = () => http.get<PanelMCP.WebsiteBinding>(`${prefix}/website/status`, {}, website);
|
||||
export const bindPanelMCPWebsite = (params: PanelMCP.WebsiteBind) =>
|
||||
http.postWithConfig<PanelMCP.WebsiteBinding>(`${prefix}/website/bind`, params, { ...local, timeout: 120000 });
|
||||
http.postWithConfig<PanelMCP.WebsiteBinding>(`${prefix}/website/bind`, params, { ...website, timeout: 120000 });
|
||||
export const updatePanelMCPWebsite = (params: PanelMCP.WebsiteUpdate) =>
|
||||
http.postWithConfig<PanelMCP.WebsiteBinding>(`${prefix}/website/update`, params, { ...local, timeout: 120000 });
|
||||
http.postWithConfig<PanelMCP.WebsiteBinding>(`${prefix}/website/update`, params, { ...website, timeout: 120000 });
|
||||
export const unbindPanelMCPWebsite = (params: PanelMCP.WebsiteUnbind) =>
|
||||
http.postWithConfig<PanelMCP.WebsiteBinding>(`${prefix}/website/unbind`, params, { ...local, timeout: 120000 });
|
||||
http.postWithConfig<PanelMCP.WebsiteBinding>(`${prefix}/website/unbind`, params, { ...website, timeout: 120000 });
|
||||
export const savePanelMCPSettings = (params: PanelMCP.Settings) =>
|
||||
http.postWithConfig(`${prefix}/settings`, params, { ...local, timeout: 60000 });
|
||||
export const operatePanelMCPService = (operation: PanelMCP.Operation) =>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import http from '@/api';
|
||||
import http, { type RequestConfig } from '@/api';
|
||||
import { ReqPage, ResPage } from '../interface';
|
||||
import { Website } from '../interface/website';
|
||||
import { File } from '../interface/file';
|
||||
@@ -124,8 +124,8 @@ export const listSSL = (req: Website.SSLReq) => {
|
||||
return http.post<Website.SSLDTO[]>(`/websites/ssl/list`, req);
|
||||
};
|
||||
|
||||
export const listLocalNodeSSL = (req: Website.SSLReq) => {
|
||||
return http.postLocalNode<Website.SSLDTO[]>(`/websites/ssl/list`, req);
|
||||
export const listLocalNodeSSL = (req: Website.SSLReq, config?: RequestConfig) => {
|
||||
return http.postLocalNode<Website.SSLDTO[]>(`/websites/ssl/list`, req, undefined, config);
|
||||
};
|
||||
|
||||
export const createSSL = (req: Website.SSLCreate) => {
|
||||
|
||||
@@ -1811,6 +1811,7 @@ const message = {
|
||||
websiteUnavailable: 'Binding unavailable',
|
||||
websiteDomainInvalid: 'Enter a valid domain, optionally including a port.',
|
||||
websiteCertificateRequired: 'Select a valid certificate covering this domain.',
|
||||
websiteCertificatesLoadFailed: 'Cannot load certificates. Refresh to retry.',
|
||||
websiteCertificateEmpty: 'No matching certificate. Apply for or import one.',
|
||||
websiteUnbind: 'Unbind and delete website',
|
||||
websiteUnbindConfirm:
|
||||
|
||||
@@ -1826,6 +1826,7 @@ const message = {
|
||||
websiteUnavailable: 'Vinculación no disponible',
|
||||
websiteDomainInvalid: 'Introduzca un dominio válido, con un puerto opcional.',
|
||||
websiteCertificateRequired: 'Seleccione un certificado válido que cubra este dominio.',
|
||||
websiteCertificatesLoadFailed: 'No se pueden cargar los certificados. Actualice para reintentar.',
|
||||
websiteCertificateEmpty: 'No hay un certificado coincidente. Solicite o importe uno.',
|
||||
websiteUnbind: 'Desvincular y eliminar sitio web',
|
||||
websiteUnbindConfirm:
|
||||
|
||||
@@ -1792,6 +1792,7 @@ const message = {
|
||||
websiteUnavailable: 'اتصال در دسترس نیست',
|
||||
websiteDomainInvalid: 'یک دامنه معتبر وارد کنید؛ میتوانید پورت را نیز مشخص کنید.',
|
||||
websiteCertificateRequired: 'یک گواهی معتبر برای این دامنه انتخاب کنید.',
|
||||
websiteCertificatesLoadFailed: 'بارگیری فهرست گواهیها ناموفق بود. تازهسازی کرده و دوباره تلاش کنید.',
|
||||
websiteCertificateEmpty: 'گواهی منطبق وجود ندارد. گواهی درخواست یا وارد کنید.',
|
||||
websiteUnbind: 'قطع اتصال و حذف وبسایت',
|
||||
websiteUnbindConfirm: 'اتصال قطع و سایت و پوشه آن حذف شود؟ گواهی حفظ میشود. اتصال مجدد سایت تازهای میسازد.',
|
||||
|
||||
@@ -1794,6 +1794,7 @@ const message = {
|
||||
websiteUnavailable: '関連付けを利用できません',
|
||||
websiteDomainInvalid: '有効なドメインを入力してください。ポートも指定できます。',
|
||||
websiteCertificateRequired: 'このドメインを対象とする有効な証明書を選択してください。',
|
||||
websiteCertificatesLoadFailed: '証明書一覧を読み込めません。更新して再試行してください。',
|
||||
websiteCertificateEmpty: '一致する証明書がありません。申請またはインポートしてください。',
|
||||
websiteUnbind: '関連付けを解除してサイトを削除',
|
||||
websiteUnbindConfirm:
|
||||
|
||||
@@ -1778,6 +1778,7 @@ const message = {
|
||||
websiteUnavailable: '연결 사용 불가',
|
||||
websiteDomainInvalid: '유효한 도메인을 입력하세요. 포트를 포함할 수 있습니다.',
|
||||
websiteCertificateRequired: '이 도메인에 유효한 인증서를 선택하세요.',
|
||||
websiteCertificatesLoadFailed: '인증서 목록을 불러올 수 없습니다. 새로 고침 후 다시 시도하세요.',
|
||||
websiteCertificateEmpty: '일치하는 인증서가 없습니다. 신청하거나 가져오세요.',
|
||||
websiteUnbind: '연결 해제 및 웹사이트 삭제',
|
||||
websiteUnbindConfirm:
|
||||
|
||||
@@ -1768,6 +1768,7 @@ const message = {
|
||||
websiteUnavailable: 'ການຜູກບໍ່ພ້ອມໃຊ້',
|
||||
websiteDomainInvalid: 'ປ້ອນໂດເມນທີ່ຖືກຕ້ອງ ໂດຍສາມາດລະບຸພອດໄດ້.',
|
||||
websiteCertificateRequired: 'ເລືອກໃບຢັ້ງຢືນທີ່ຍັງໃຊ້ໄດ້ ແລະຄອບຄຸມໂດເມນນີ້.',
|
||||
websiteCertificatesLoadFailed: 'ບໍ່ສາມາດໂຫຼດລາຍການໃບຮັບຮອງໄດ້. ກະລຸນາໂຫຼດໜ້າໃໝ່ແລ້ວລອງອີກຄັ້ງ.',
|
||||
websiteCertificateEmpty: 'ບໍ່ມີໃບຮັບຮອງທີ່ກົງກັນ. ຂໍ ຫຼືນຳເຂົ້າໃບຮັບຮອງ.',
|
||||
websiteUnbind: 'ຍົກເລີກການຜູກ ແລະ ລຶບເວັບໄຊ',
|
||||
websiteUnbindConfirm: 'ຍົກເລີກການຜູກ ແລະລຶບເວັບໄຊພ້ອມໂຟນເດີບໍ? ເກັບໃບຮັບຮອງໄວ້. ຜູກໃໝ່ຈະສ້າງເວັບໄຊໃໝ່.',
|
||||
|
||||
@@ -1825,6 +1825,7 @@ const message = {
|
||||
websiteUnavailable: 'Pautan tidak tersedia',
|
||||
websiteDomainInvalid: 'Masukkan domain yang sah, dengan port pilihan.',
|
||||
websiteCertificateRequired: 'Pilih sijil sah yang meliputi domain ini.',
|
||||
websiteCertificatesLoadFailed: 'Tidak dapat memuatkan senarai sijil. Muat semula untuk mencuba lagi.',
|
||||
websiteCertificateEmpty: 'Tiada sijil sepadan. Mohon atau import sijil.',
|
||||
websiteUnbind: 'Nyahpaut dan padam laman web',
|
||||
websiteUnbindConfirm:
|
||||
|
||||
@@ -1825,6 +1825,7 @@ const message = {
|
||||
websiteUnavailable: 'Vinculação indisponível',
|
||||
websiteDomainInvalid: 'Informe um domínio válido, com uma porta opcional.',
|
||||
websiteCertificateRequired: 'Selecione um certificado válido que cubra este domínio.',
|
||||
websiteCertificatesLoadFailed: 'Não foi possível carregar os certificados. Atualize para tentar novamente.',
|
||||
websiteCertificateEmpty: 'Nenhum certificado correspondente. Solicite ou importe um.',
|
||||
websiteUnbind: 'Desvincular e excluir site',
|
||||
websiteUnbindConfirm:
|
||||
|
||||
@@ -1815,6 +1815,7 @@ const message = {
|
||||
websiteUnavailable: 'Привязка недоступна',
|
||||
websiteDomainInvalid: 'Введите допустимый домен, при необходимости с портом.',
|
||||
websiteCertificateRequired: 'Выберите действующий сертификат для этого домена.',
|
||||
websiteCertificatesLoadFailed: 'Не удалось загрузить список сертификатов. Обновите и повторите попытку.',
|
||||
websiteCertificateEmpty: 'Нет подходящего сертификата. Выпустите или импортируйте его.',
|
||||
websiteUnbind: 'Отвязать и удалить сайт',
|
||||
websiteUnbindConfirm:
|
||||
|
||||
@@ -1820,6 +1820,7 @@ const message = {
|
||||
websiteUnavailable: 'Bağlantı kullanılamıyor',
|
||||
websiteDomainInvalid: 'Geçerli bir alan adı girin; isteğe bağlı olarak bağlantı noktası ekleyebilirsiniz.',
|
||||
websiteCertificateRequired: 'Bu alan adını kapsayan geçerli bir sertifika seçin.',
|
||||
websiteCertificatesLoadFailed: 'Sertifika listesi yüklenemedi. Yenileyip tekrar deneyin.',
|
||||
websiteCertificateEmpty: 'Uygun sertifika yok. Başvurun veya içe aktarın.',
|
||||
websiteUnbind: 'Bağlantıyı kaldır ve siteyi sil',
|
||||
websiteUnbindConfirm:
|
||||
|
||||
@@ -1717,6 +1717,7 @@ const message = {
|
||||
websiteUnavailable: '綁定不可用',
|
||||
websiteDomainInvalid: '請輸入有效網域,可包含連接埠。',
|
||||
websiteCertificateRequired: '請選擇涵蓋此網域且有效的憑證。',
|
||||
websiteCertificatesLoadFailed: '憑證清單讀取失敗,請重新整理後重試。',
|
||||
websiteCertificateEmpty: '無符合憑證,請先申請或匯入。',
|
||||
websiteUnbind: '解除綁定並刪除網站',
|
||||
websiteUnbindConfirm: '解除綁定並刪除網站及目錄?憑證保留,重新綁定將建立新網站。',
|
||||
|
||||
@@ -1731,6 +1731,7 @@ const message = {
|
||||
websiteUnavailable: '绑定不可用',
|
||||
websiteDomainInvalid: '请输入有效域名,可包含端口。',
|
||||
websiteCertificateRequired: '请选择覆盖该域名且有效的证书。',
|
||||
websiteCertificatesLoadFailed: '证书列表读取失败,请刷新重试。',
|
||||
websiteCertificateEmpty: '暂无匹配证书,请先申请或导入。',
|
||||
websiteUnbind: '解绑并删除网站',
|
||||
websiteUnbindConfirm: '解绑并删除网站及目录?证书保留,重新绑定将新建网站。',
|
||||
|
||||
@@ -4,23 +4,22 @@
|
||||
<span class="font-medium">{{ $t('panelMCP.websiteBinding') }}</span>
|
||||
<el-tag
|
||||
v-if="binding"
|
||||
:type="binding.state === 'bound' ? 'success' : binding.state === 'unbound' ? 'info' : 'warning'"
|
||||
:type="
|
||||
pending
|
||||
? 'warning'
|
||||
: binding.state === 'bound'
|
||||
? 'success'
|
||||
: binding.state === 'unbound'
|
||||
? 'info'
|
||||
: 'warning'
|
||||
"
|
||||
size="small"
|
||||
>
|
||||
{{ $t(`panelMCP.${stateLabels[binding.state]}`) }}
|
||||
{{ $t(`panelMCP.${stateLabels[pending ? 'pending' : binding.state]}`) }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<el-alert v-if="errorKey" class="!mb-4" type="warning" :closable="false" :title="$t(errorKey)" show-icon />
|
||||
<el-alert
|
||||
v-else-if="binding?.state === 'pending' || uncertain"
|
||||
class="!mb-4"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
:title="$t('panelMCP.websiteUncertain')"
|
||||
show-icon
|
||||
/>
|
||||
<el-alert
|
||||
v-else-if="binding?.state === 'unavailable'"
|
||||
v-if="binding?.state === 'unavailable'"
|
||||
class="!mb-4"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
@@ -109,8 +108,6 @@
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="pendingOperation?.operation === 'bind' || pendingOperation?.operation === 'update'"
|
||||
type="danger"
|
||||
plain
|
||||
:disabled="disabled || !canManage || busy"
|
||||
@click="cancelPending"
|
||||
>
|
||||
@@ -130,7 +127,7 @@
|
||||
<el-button :disabled="disabled || !canManage || busy" @click="editCertificate">
|
||||
{{ $t('panelMCP.websiteCertificateUpdate') }}
|
||||
</el-button>
|
||||
<el-button type="danger" plain :disabled="disabled || !canManage || busy" @click="unbind">
|
||||
<el-button :disabled="disabled || !canManage || busy" @click="unbind">
|
||||
{{ $t('panelMCP.websiteUnbind') }}
|
||||
</el-button>
|
||||
</template>
|
||||
@@ -157,6 +154,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, ref, watch } from 'vue';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { isAxiosError } from 'axios';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { PanelMCP } from '@/api/interface/panel-mcp';
|
||||
import {
|
||||
@@ -166,18 +164,12 @@ import {
|
||||
updatePanelMCPWebsite,
|
||||
} from '@/api/modules/panel-mcp';
|
||||
import { listLocalNodeSSL } from '@/api/modules/website';
|
||||
import {
|
||||
getMCPConflict,
|
||||
getMCPWebsiteHostname,
|
||||
isMCPMutationUncertain,
|
||||
isMCPRevisionConflict,
|
||||
isMCPWebsiteCertificateMatch,
|
||||
} from '@/utils/panel-mcp';
|
||||
import { getMCPWebsiteHostname, isMCPMutationUncertain, isMCPWebsiteCertificateMatch } from '@/utils/panel-mcp';
|
||||
import { dateFormatSimple } from '@/utils/date';
|
||||
import { listNodes } from '@/utils/node';
|
||||
import { routerToNameWithParams } from '@/utils/router';
|
||||
import { useGlobalStore } from '@/composables/useGlobalStore';
|
||||
import { MsgSuccess } from '@/utils/message';
|
||||
import { MsgError, MsgSuccess } from '@/utils/message';
|
||||
import i18n from '@/lang';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -193,7 +185,7 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
const { globalStore } = useGlobalStore();
|
||||
type Certificate = { id: number; primaryDomain: string; otherDomains: string; expireDate: string };
|
||||
type PendingOperation = PanelMCP.WebsitePendingRequest;
|
||||
type PendingOperation = PanelMCP.WebsitePendingRequest & { revision?: number };
|
||||
const stateLabels: Record<PanelMCP.WebsiteBinding['state'], string> = {
|
||||
unbound: 'websiteUnbound',
|
||||
pending: 'websitePending',
|
||||
@@ -208,13 +200,14 @@ const certificatesLoaded = ref(false);
|
||||
const certificates = ref<Certificate[]>([]);
|
||||
const editing = ref(false);
|
||||
const uncertain = ref(false);
|
||||
const errorKey = ref('');
|
||||
const domainError = ref('');
|
||||
const certificateError = ref('');
|
||||
const localPending = ref<PendingOperation>();
|
||||
const busy = computed(() => reading.value || writing.value);
|
||||
const associated = computed(() => !!binding.value && binding.value.state !== 'unbound');
|
||||
const pendingOperation = computed(() => localPending.value || binding.value?.pendingRequest);
|
||||
const pendingOperation = computed<PendingOperation | undefined>(
|
||||
() => localPending.value || binding.value?.pendingRequest,
|
||||
);
|
||||
const pending = computed(() => binding.value?.state === 'pending' || !!pendingOperation.value || uncertain.value);
|
||||
const showEditor = computed(
|
||||
() => (!associated.value && (props.mode === 'settings' || props.draft.requested)) || editing.value,
|
||||
@@ -236,9 +229,37 @@ let generation = 0;
|
||||
let readVersion = 0;
|
||||
let navigationVersion = 0;
|
||||
let certificateVersion = 0;
|
||||
let certificatePromise: Promise<void> | undefined;
|
||||
let certificatePromise: Promise<boolean> | undefined;
|
||||
const current = (version: number) => active && props.canManage && globalStore.isLogin && version === generation;
|
||||
|
||||
const notifyFailure = (error: unknown, key: string) => {
|
||||
const requestError = isAxiosError(error);
|
||||
if (requestError && /timeout/i.test(error.message)) {
|
||||
MsgError(i18n.global.t('commons.msg.requestTimeout'));
|
||||
return;
|
||||
}
|
||||
const data = (requestError ? error.response?.data : error) as { code?: unknown; message?: unknown } | undefined;
|
||||
const message =
|
||||
data && (requestError || typeof data.code === 'number') && typeof data.message === 'string'
|
||||
? data.message.trim()
|
||||
: '';
|
||||
MsgError(
|
||||
message.toLowerCase().includes('operation not permitted')
|
||||
? i18n.global.t('license.tamperHelper')
|
||||
: message || i18n.global.t(key),
|
||||
);
|
||||
};
|
||||
const completed = (value: PanelMCP.WebsiteBinding, operation: PendingOperation) =>
|
||||
value.requestId === operation.requestId &&
|
||||
!value.pendingRequest &&
|
||||
value.state === (operation.operation === 'unbind' ? 'unbound' : 'bound') &&
|
||||
(operation.operation !== 'unbind' || operation.deleteWebsite !== true || value.websiteDeleted === true);
|
||||
const finish = (value: PanelMCP.WebsiteBinding) => {
|
||||
localPending.value = undefined;
|
||||
uncertain.value = editing.value = false;
|
||||
props.draft.requested = false;
|
||||
emit('changed', value);
|
||||
};
|
||||
const acceptBinding = (value: PanelMCP.WebsiteBinding, syncDraft = true) => {
|
||||
binding.value = value;
|
||||
if (syncDraft && value.pendingRequest) {
|
||||
@@ -277,39 +298,62 @@ const validBinding = (value: unknown): value is PanelMCP.WebsiteBinding => {
|
||||
};
|
||||
const read = async (version: number, syncDraft = true) => {
|
||||
const ticket = ++readVersion;
|
||||
const operation = pendingOperation.value;
|
||||
const response = await getPanelMCPWebsite();
|
||||
if (!current(version) || ticket !== readVersion) return;
|
||||
if (!validBinding(response.data)) throw new Error('Invalid MCP website status');
|
||||
if (!validBinding(response?.data)) throw new Error('Invalid MCP website status');
|
||||
acceptBinding(response.data, syncDraft);
|
||||
if (operation && completed(response.data, operation)) finish(response.data);
|
||||
return response.data;
|
||||
};
|
||||
const load = async (refreshCertificates = false) => {
|
||||
if (!active || !props.canManage || busy.value || props.disabled) return;
|
||||
const version = generation;
|
||||
reading.value = true;
|
||||
const request = read(version, !refreshCertificates);
|
||||
const ticket = readVersion;
|
||||
const activeRead = () => current(version) && ticket === readVersion;
|
||||
try {
|
||||
const requests: Promise<unknown>[] = [read(version, !refreshCertificates)];
|
||||
if (refreshCertificates) requests.push(loadCertificates(true));
|
||||
const [status] = await Promise.allSettled(requests);
|
||||
if (current(version)) {
|
||||
errorKey.value = status.status === 'fulfilled' ? '' : 'panelMCP.websiteStatusLoadFailed';
|
||||
const status = request
|
||||
.catch((error) => {
|
||||
if (activeRead()) notifyFailure(error, 'panelMCP.websiteStatusLoadFailed');
|
||||
throw error;
|
||||
})
|
||||
.finally(() => {
|
||||
if (!refreshCertificates && activeRead()) reading.value = false;
|
||||
});
|
||||
const requests: Promise<unknown>[] = [status];
|
||||
if (refreshCertificates || (props.draft.enableSSL && showEditor.value)) {
|
||||
requests.push(loadCertificates(refreshCertificates, false));
|
||||
}
|
||||
const [result, certificate] = await Promise.allSettled(requests);
|
||||
if (
|
||||
activeRead() &&
|
||||
result.status === 'fulfilled' &&
|
||||
certificate?.status === 'fulfilled' &&
|
||||
certificate.value === false &&
|
||||
(refreshCertificates || (props.draft.enableSSL && showEditor.value))
|
||||
) {
|
||||
notifyFailure(undefined, 'panelMCP.websiteCertificatesLoadFailed');
|
||||
}
|
||||
} finally {
|
||||
if (current(version)) reading.value = false;
|
||||
if (activeRead()) reading.value = false;
|
||||
}
|
||||
};
|
||||
const refresh = () => load(true);
|
||||
const loadCertificates = async (force = false) => {
|
||||
if (!active || !props.canManage) return;
|
||||
const loadCertificates = async (force = false, notify = true): Promise<boolean> => {
|
||||
if (!active || !props.canManage || !globalStore.isLogin) return false;
|
||||
if (certificatePromise && !force) return certificatePromise;
|
||||
if (certificatesLoaded.value && !force) return;
|
||||
if (certificatesLoaded.value && !force) return true;
|
||||
const version = generation;
|
||||
const ticket = ++certificateVersion;
|
||||
certificatesBusy.value = true;
|
||||
certificateError.value = '';
|
||||
const loading = (async () => {
|
||||
try {
|
||||
const response = await listLocalNodeSSL({});
|
||||
if (!current(version) || ticket !== certificateVersion) return;
|
||||
const response = await listLocalNodeSSL({}, { errorMessageMode: 'manual' });
|
||||
if (!current(version) || ticket !== certificateVersion) return false;
|
||||
if (!Array.isArray(response?.data)) throw new Error('Invalid MCP certificate list');
|
||||
certificates.value = response.data.map((item) => ({
|
||||
id: item.id,
|
||||
primaryDomain: item.primaryDomain,
|
||||
@@ -318,17 +362,23 @@ const loadCertificates = async (force = false) => {
|
||||
}));
|
||||
certificatesLoaded.value = true;
|
||||
certificateError.value = '';
|
||||
} catch {
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (current(version) && ticket === certificateVersion) {
|
||||
certificateError.value = i18n.global.t('panelMCP.websiteCertificateRequired');
|
||||
certificatesLoaded.value = false;
|
||||
if (notify && props.draft.enableSSL && showEditor.value) {
|
||||
notifyFailure(error, 'panelMCP.websiteCertificatesLoadFailed');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
if (ticket === certificateVersion) certificatesBusy.value = false;
|
||||
}
|
||||
})();
|
||||
certificatePromise = loading;
|
||||
await loading;
|
||||
const loaded = await loading;
|
||||
if (certificatePromise === loading) certificatePromise = undefined;
|
||||
return loaded;
|
||||
};
|
||||
const validate = async () => {
|
||||
domainError.value = certificateError.value = '';
|
||||
@@ -339,7 +389,8 @@ const validate = async () => {
|
||||
return false;
|
||||
}
|
||||
if (props.draft.enableSSL) {
|
||||
await loadCertificates();
|
||||
const version = generation;
|
||||
if (!(await loadCertificates()) || !current(version)) return false;
|
||||
if (!matchingCertificates.value.some((item) => item.id === props.draft.sslID)) {
|
||||
certificateError.value = i18n.global.t('panelMCP.websiteCertificateRequired');
|
||||
return false;
|
||||
@@ -352,13 +403,16 @@ const perform = async (operation: PendingOperation): Promise<boolean> => {
|
||||
const version = generation;
|
||||
writing.value = true;
|
||||
emit('busy', true);
|
||||
errorKey.value = '';
|
||||
localPending.value = { ...operation };
|
||||
const deletesWebsite = operation.operation === 'unbind' && operation.deleteWebsite === true;
|
||||
try {
|
||||
const latest = binding.value || (await read(version));
|
||||
if (!current(version) || !latest) return false;
|
||||
const request = { requestId: operation.requestId, revision: latest.revision };
|
||||
// A retry must keep the revision that authorized the original intent.
|
||||
const revision = operation.revision ?? latest.revision;
|
||||
operation = { ...operation, revision };
|
||||
localPending.value = operation;
|
||||
const request = { requestId: operation.requestId, revision };
|
||||
const response =
|
||||
operation.operation === 'bind'
|
||||
? await bindPanelMCPWebsite({
|
||||
@@ -375,54 +429,30 @@ const perform = async (operation: PendingOperation): Promise<boolean> => {
|
||||
})
|
||||
: await unbindPanelMCPWebsite({
|
||||
...request,
|
||||
...(deletesWebsite ? { deleteWebsite: true } : {}),
|
||||
...(operation.deleteWebsite !== undefined ? { deleteWebsite: operation.deleteWebsite } : {}),
|
||||
});
|
||||
if (!current(version)) return false;
|
||||
if (!validBinding(response.data)) throw { code: 503 };
|
||||
if (!validBinding(response?.data) || response.data.requestId !== operation.requestId) throw { code: 503 };
|
||||
if (deletesWebsite && response.data.websiteDeleted !== true) throw { code: 503 };
|
||||
acceptBinding(response.data);
|
||||
if (response.data.state !== (operation.operation === 'unbind' ? 'unbound' : 'bound')) throw { code: 503 };
|
||||
localPending.value = undefined;
|
||||
uncertain.value = false;
|
||||
editing.value = false;
|
||||
props.draft.requested = false;
|
||||
emit('changed', response.data);
|
||||
if (!completed(response.data, operation)) throw { code: 503 };
|
||||
finish(response.data);
|
||||
MsgSuccess(i18n.global.t('commons.msg.operationSuccess'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (current(version)) {
|
||||
uncertain.value = isMCPMutationUncertain(error);
|
||||
errorKey.value = isMCPRevisionConflict(error)
|
||||
? 'panelMCP.websiteConflict'
|
||||
: uncertain.value
|
||||
? 'panelMCP.websiteUncertain'
|
||||
: 'panelMCP.websiteOperationFailed';
|
||||
const conflict = getMCPConflict(error);
|
||||
if (conflict?.reason === 'website_domain_conflict' && typeof conflict.message === 'string') {
|
||||
domainError.value = conflict.message;
|
||||
errorKey.value = '';
|
||||
}
|
||||
const unconfirmed = isMCPMutationUncertain(error);
|
||||
uncertain.value = unconfirmed;
|
||||
if (!uncertain.value) localPending.value = undefined;
|
||||
try {
|
||||
const verified = await read(version);
|
||||
if (
|
||||
uncertain.value &&
|
||||
verified &&
|
||||
verified.requestId === operation.requestId &&
|
||||
!verified.pendingRequest &&
|
||||
(!deletesWebsite || verified.websiteDeleted === true) &&
|
||||
verified.state === (operation.operation === 'unbind' ? 'unbound' : 'bound')
|
||||
) {
|
||||
localPending.value = undefined;
|
||||
uncertain.value = editing.value = false;
|
||||
errorKey.value = '';
|
||||
props.draft.requested = false;
|
||||
emit('changed', verified);
|
||||
return true;
|
||||
}
|
||||
if (unconfirmed && verified && completed(verified, operation)) return true;
|
||||
} catch {
|
||||
/* Keep the submitted operation until the user can check its result. */
|
||||
}
|
||||
if (current(version)) {
|
||||
notifyFailure(error, unconfirmed ? 'panelMCP.websiteUncertain' : 'panelMCP.websiteOperationFailed');
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} finally {
|
||||
@@ -470,7 +500,7 @@ const requestUnbind = async (cancelPending: boolean) => {
|
||||
await ElMessageBox.confirm(
|
||||
i18n.global.t('panelMCP.websiteUnbindConfirm'),
|
||||
i18n.global.t('panelMCP.websiteUnbind'),
|
||||
{ type: 'warning', confirmButtonType: 'danger' },
|
||||
{ type: 'warning' },
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
@@ -502,7 +532,6 @@ const activate = () => {
|
||||
if (active) return;
|
||||
active = true;
|
||||
if (props.canManage) void load();
|
||||
if (props.draft.enableSSL && showEditor.value) void loadCertificates();
|
||||
};
|
||||
const invalidate = () => {
|
||||
generation++;
|
||||
@@ -516,7 +545,7 @@ const invalidate = () => {
|
||||
reading.value = false;
|
||||
editing.value = uncertain.value = false;
|
||||
localPending.value = undefined;
|
||||
domainError.value = certificateError.value = errorKey.value = '';
|
||||
domainError.value = certificateError.value = '';
|
||||
emit('binding', undefined);
|
||||
};
|
||||
const deactivate = () => {
|
||||
|
||||
Reference in New Issue
Block a user