Protect CMS deploy password field
This commit is contained in:
parent
dc49acaa43
commit
cfee9d0c19
|
|
@ -970,6 +970,46 @@ body:not([data-admin-mode="knowledge"]) #knowledge-form {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.secret-input-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-input-wrap input {
|
||||||
|
padding-right: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: 0.42rem;
|
||||||
|
display: grid;
|
||||||
|
width: 2.1rem;
|
||||||
|
height: 2.1rem;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--launcher-radius-circle);
|
||||||
|
background: rgba(255, 255, 255, 0.065);
|
||||||
|
color: rgba(255, 255, 255, 0.72);
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle-button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.12);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secret-toggle-button svg {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
fill: none;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.8;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
|
||||||
.field-label-row {
|
.field-label-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
|
||||||
|
|
@ -640,6 +640,14 @@ function syncProjectConfigFromForm() {
|
||||||
|
|
||||||
async function saveProjectSettings() {
|
async function saveProjectSettings() {
|
||||||
syncProjectConfigFromForm();
|
syncProjectConfigFromForm();
|
||||||
|
const passwordEnv = String(state.projectConfig?.deploy?.passwordEnv || "").trim();
|
||||||
|
if (passwordEnv && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(passwordEnv)) {
|
||||||
|
setByPath(state.projectConfig, "deploy.passwordEnv", "MCHOST_PASSWORD");
|
||||||
|
renderProjectSettings();
|
||||||
|
setStatus("FTP-пароль не сохраняем в модель. Поле сброшено к имени переменной MCHOST_PASSWORD.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch("/api/project/config", {
|
const response = await fetch("/api/project/config", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: { "content-type": "application/json" },
|
headers: { "content-type": "application/json" },
|
||||||
|
|
@ -1932,6 +1940,7 @@ function createProjectField({ path, label, type = "text", placeholder = "", opti
|
||||||
const badge = document.createElement("span");
|
const badge = document.createElement("span");
|
||||||
const control = options.length ? document.createElement("select") : document.createElement("input");
|
const control = options.length ? document.createElement("select") : document.createElement("input");
|
||||||
const value = projectConfigValue(path);
|
const value = projectConfigValue(path);
|
||||||
|
const isSecret = type === "password";
|
||||||
|
|
||||||
field.className = type === "checkbox" ? "toggle-row" : "field-control";
|
field.className = type === "checkbox" ? "toggle-row" : "field-control";
|
||||||
title.textContent = label;
|
title.textContent = label;
|
||||||
|
|
@ -1956,8 +1965,30 @@ function createProjectField({ path, label, type = "text", placeholder = "", opti
|
||||||
}
|
}
|
||||||
|
|
||||||
badge.className = "field-kind";
|
badge.className = "field-kind";
|
||||||
badge.textContent = options.length ? "SELECT" : type === "number" ? "NUMBER" : "TEXT";
|
badge.textContent = options.length ? "SELECT" : type === "number" ? "NUMBER" : isSecret ? "SECRET" : "TEXT";
|
||||||
field.append(title, badge, control);
|
field.append(title, badge);
|
||||||
|
|
||||||
|
if (isSecret) {
|
||||||
|
const secretWrap = document.createElement("span");
|
||||||
|
const reveal = document.createElement("button");
|
||||||
|
secretWrap.className = "secret-input-wrap";
|
||||||
|
reveal.className = "secret-toggle-button";
|
||||||
|
reveal.type = "button";
|
||||||
|
reveal.innerHTML =
|
||||||
|
'<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M2.75 12s3.25-5.5 9.25-5.5 9.25 5.5 9.25 5.5-3.25 5.5-9.25 5.5S2.75 12 2.75 12Z"></path><path d="M12 9.25a2.75 2.75 0 1 1 0 5.5 2.75 2.75 0 0 1 0-5.5Z"></path></svg>';
|
||||||
|
reveal.setAttribute("aria-label", "Показать значение");
|
||||||
|
reveal.title = "Показать значение";
|
||||||
|
reveal.addEventListener("click", () => {
|
||||||
|
const visible = control.type === "text";
|
||||||
|
control.type = visible ? "password" : "text";
|
||||||
|
reveal.setAttribute("aria-label", visible ? "Показать значение" : "Скрыть значение");
|
||||||
|
reveal.title = visible ? "Показать значение" : "Скрыть значение";
|
||||||
|
});
|
||||||
|
secretWrap.append(control, reveal);
|
||||||
|
field.append(secretWrap);
|
||||||
|
} else {
|
||||||
|
field.append(control);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
control.dataset.projectPath = path;
|
control.dataset.projectPath = path;
|
||||||
|
|
@ -2113,7 +2144,13 @@ function renderProjectSettings() {
|
||||||
createProjectField({ path: "deploy.host", label: "FTP / SFTP host", placeholder: "a0000.ftp.mchost.ru" }),
|
createProjectField({ path: "deploy.host", label: "FTP / SFTP host", placeholder: "a0000.ftp.mchost.ru" }),
|
||||||
createProjectField({ path: "deploy.port", label: "Порт", type: "number", placeholder: "22" }),
|
createProjectField({ path: "deploy.port", label: "Порт", type: "number", placeholder: "22" }),
|
||||||
createProjectField({ path: "deploy.username", label: "Логин" }),
|
createProjectField({ path: "deploy.username", label: "Логин" }),
|
||||||
createProjectField({ path: "deploy.passwordEnv", label: "Переменная пароля", placeholder: "MCHOST_PASSWORD" }),
|
createProjectField({
|
||||||
|
path: "deploy.passwordEnv",
|
||||||
|
label: "Имя переменной пароля",
|
||||||
|
type: "password",
|
||||||
|
placeholder: "MCHOST_PASSWORD",
|
||||||
|
hint: "Не вводите сюда сам FTP-пароль. Укажите имя переменной окружения сервиса, например MCHOST_PASSWORD.",
|
||||||
|
}),
|
||||||
createProjectField({ path: "deploy.remotePath", label: "Папка сайта на хостинге", placeholder: "httpdocs" }),
|
createProjectField({ path: "deploy.remotePath", label: "Папка сайта на хостинге", placeholder: "httpdocs" }),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,11 @@ function trailingSlashUrl(value) {
|
||||||
return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
return trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSecretEnvName(value, fallback = "MCHOST_PASSWORD") {
|
||||||
|
const trimmed = String(value || "").trim();
|
||||||
|
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed) ? trimmed : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function fileReady(filePath) {
|
function fileReady(filePath) {
|
||||||
try {
|
try {
|
||||||
return statSync(filePath).isFile();
|
return statSync(filePath).isFile();
|
||||||
|
|
@ -809,8 +814,14 @@ function authorizeRequest(req, res, url) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function publicProjectConfig() {
|
function publicProjectConfig() {
|
||||||
|
const deploy = {
|
||||||
|
...(projectConfig.deploy || {}),
|
||||||
|
passwordEnv: normalizeSecretEnvName(projectConfig.deploy?.passwordEnv),
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...projectConfig,
|
...projectConfig,
|
||||||
|
deploy,
|
||||||
id: projectConfig.id || activeProjectId,
|
id: projectConfig.id || activeProjectId,
|
||||||
runtime: {
|
runtime: {
|
||||||
cmsRoot,
|
cmsRoot,
|
||||||
|
|
@ -829,7 +840,7 @@ function normalizeProjectConfig(payload) {
|
||||||
host: String(payload?.deploy?.host || "").trim(),
|
host: String(payload?.deploy?.host || "").trim(),
|
||||||
port: Number(payload?.deploy?.port || (payload?.deploy?.method === "ftp" ? 21 : 22)),
|
port: Number(payload?.deploy?.port || (payload?.deploy?.method === "ftp" ? 21 : 22)),
|
||||||
username: String(payload?.deploy?.username || "").trim(),
|
username: String(payload?.deploy?.username || "").trim(),
|
||||||
passwordEnv: String(payload?.deploy?.passwordEnv || "").trim(),
|
passwordEnv: normalizeSecretEnvName(payload?.deploy?.passwordEnv),
|
||||||
remotePath: String(payload?.deploy?.remotePath || "").trim(),
|
remotePath: String(payload?.deploy?.remotePath || "").trim(),
|
||||||
deployOnRender: Boolean(payload?.deploy?.deployOnRender),
|
deployOnRender: Boolean(payload?.deploy?.deployOnRender),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue