v0.1.4-pre.013
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/environment.ts
|
||||
// version: 3
|
||||
// version: 4
|
||||
|
||||
//! Safe environment report and `.env` management panel backed exclusively by ConfigManagement.
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ConfigEnvironmentChangeDto } from "./bindings/ksp_app_config_desk/
|
||||
import type { ConfigEnvironmentReportDto } from "./bindings/ksp_app_config_desk/environment/ConfigEnvironmentReportDto";
|
||||
import { frontendDebug, frontendTrace } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
import { initializeSecretReveal, requestSecretReveal } from "./secret_reveal";
|
||||
|
||||
interface EnvironmentDataTable {
|
||||
destroy(): unknown;
|
||||
@@ -17,6 +18,7 @@ interface EnvironmentDataTable {
|
||||
|
||||
let environmentTable: EnvironmentDataTable | null = null;
|
||||
let currentReports: ConfigEnvironmentReportDto[] = [];
|
||||
let mutationControlsDisabled = false;
|
||||
|
||||
function setEnvironmentStatus(message: string, tone: "primary" | "success" | "warning" | "danger" = "primary"): void {
|
||||
const status = document.querySelector<HTMLElement>("#environmentStatus");
|
||||
@@ -163,9 +165,16 @@ function updateEditorSensitivity(): void {
|
||||
: sensitivity === "invalid"
|
||||
? "Saisis un nom KSP_* ou KSPB_* ; Config reste l'autorité de validation."
|
||||
: `${sensitivityLabel(sensitivity)} : la valeur saisie n'est jamais préchargée depuis .env.`;
|
||||
updateRevealButtons(sensitivity);
|
||||
frontendTrace("main", "Environment mutation editor sensitivity updated", { sensitivity });
|
||||
}
|
||||
|
||||
function updateRevealButtons(sensitivity = classifyEditorSensitivity(document.querySelector<HTMLInputElement>("#environmentVariableName")?.value.trim() ?? "")): void {
|
||||
const revealDisabled = mutationControlsDisabled || sensitivity !== "secret";
|
||||
document.querySelector<HTMLButtonElement>("#revealEnvironmentEffective")?.toggleAttribute("disabled", revealDisabled);
|
||||
document.querySelector<HTMLButtonElement>("#revealEnvironmentDotenv")?.toggleAttribute("disabled", revealDisabled);
|
||||
}
|
||||
|
||||
function selectVariableForMutation(variableName: string, source: "table" | "input"): void {
|
||||
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
|
||||
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
|
||||
@@ -218,8 +227,10 @@ function renderMutationResult(result: ConfigEnvironmentChangeDto): void {
|
||||
}
|
||||
|
||||
function setMutationControlsDisabled(disabled: boolean): void {
|
||||
mutationControlsDisabled = disabled;
|
||||
document.querySelector<HTMLButtonElement>("#setEnvironmentValue")?.toggleAttribute("disabled", disabled);
|
||||
document.querySelector<HTMLButtonElement>("#removeEnvironmentValue")?.toggleAttribute("disabled", disabled);
|
||||
updateRevealButtons();
|
||||
}
|
||||
|
||||
function confirmEnvironmentRemoval(variableName: string): Promise<boolean> {
|
||||
@@ -380,6 +391,14 @@ function bindEnvironmentActions(): void {
|
||||
frontendTrace("main", "Environment remove value button clicked");
|
||||
void removeEnvironmentValue();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#revealEnvironmentEffective")?.addEventListener("click", () => {
|
||||
const variableName = document.querySelector<HTMLInputElement>("#environmentVariableName")?.value.trim() ?? "";
|
||||
requestSecretReveal(variableName, "effective");
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#revealEnvironmentDotenv")?.addEventListener("click", () => {
|
||||
const variableName = document.querySelector<HTMLInputElement>("#environmentVariableName")?.value.trim() ?? "";
|
||||
requestSecretReveal(variableName, "dotenv");
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#clearEnvironmentEditor")?.addEventListener("click", () => {
|
||||
const variable = document.querySelector<HTMLInputElement>("#environmentVariableName");
|
||||
const value = document.querySelector<HTMLInputElement>("#environmentVariableValue");
|
||||
@@ -396,6 +415,7 @@ function bindEnvironmentActions(): void {
|
||||
}
|
||||
|
||||
export function initializeEnvironmentPanel(): void {
|
||||
initializeSecretReveal();
|
||||
bindEnvironmentActions();
|
||||
updateEditorSensitivity();
|
||||
void refreshEnvironmentReport();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/main.ts
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
import "bootstrap";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
@@ -11,6 +11,7 @@ import { initializeDocumentsPanel } from "./documents";
|
||||
import { initializeEnvironmentPanel } from "./environment";
|
||||
import { initializeProfilesPanel } from "./profiles";
|
||||
import { invokeKsp } from "./invoke";
|
||||
import { clearTransientSecretReveal } from "./secret_reveal";
|
||||
|
||||
(window as Window & typeof globalThis & { ResizeObserver?: typeof ResizeObserver }).ResizeObserver = ResizeObserver;
|
||||
installFrontendConsoleBridge("main");
|
||||
@@ -45,6 +46,9 @@ function isViewId(value: string): value is ViewId {
|
||||
}
|
||||
|
||||
function activateView(viewId: ViewId, source: "startup" | "user"): void {
|
||||
if (viewId !== "environment") {
|
||||
clearTransientSecretReveal();
|
||||
}
|
||||
if (source === "user") {
|
||||
frontendDebug("main", "Main navigation activated", { viewId });
|
||||
}
|
||||
|
||||
206
crates/ksp-app-config-desk/frontend/ts/secret_reveal.ts
Normal file
206
crates/ksp-app-config-desk/frontend/ts/secret_reveal.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
// file: crates/ksp-app-config-desk/frontend/ts/secret_reveal.ts
|
||||
// version: 1
|
||||
|
||||
import { Modal } from "bootstrap";
|
||||
import type { SecretRevealRequestDto } from "./bindings/ksp_app_config_desk/secrets/SecretRevealRequestDto";
|
||||
import type { SecretRevealResponseDto } from "./bindings/ksp_app_config_desk/secrets/SecretRevealResponseDto";
|
||||
import { frontendDebug, frontendTrace } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
|
||||
export type SecretRevealSource = "effective" | "dotenv";
|
||||
|
||||
let pendingRequest: SecretRevealRequestDto | null = null;
|
||||
let revealInFlight = false;
|
||||
let revealGeneration = 0;
|
||||
|
||||
function sourceLabel(source: SecretRevealSource): string {
|
||||
return source === "effective" ? "effective (process prioritaire sur .env)" : ".env persisté";
|
||||
}
|
||||
|
||||
function modalElement(): HTMLElement | null {
|
||||
return document.querySelector<HTMLElement>("#secretRevealModal");
|
||||
}
|
||||
|
||||
function clearSecretValue(): void {
|
||||
const input = document.querySelector<HTMLInputElement>("#secretRevealValue");
|
||||
const absent = document.querySelector<HTMLElement>("#secretRevealValueAbsent");
|
||||
const result = document.querySelector<HTMLElement>("#secretRevealResultPanel");
|
||||
const confirmation = document.querySelector<HTMLElement>("#secretRevealConfirmPanel");
|
||||
const confirm = document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction");
|
||||
const toggle = document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility");
|
||||
if (input) {
|
||||
input.value = "";
|
||||
input.type = "password";
|
||||
}
|
||||
if (absent) {
|
||||
absent.hidden = true;
|
||||
}
|
||||
if (result) {
|
||||
result.hidden = true;
|
||||
}
|
||||
if (confirmation) {
|
||||
confirmation.hidden = false;
|
||||
}
|
||||
if (confirm) {
|
||||
confirm.hidden = false;
|
||||
confirm.disabled = false;
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.hidden = true;
|
||||
toggle.textContent = "Afficher";
|
||||
}
|
||||
}
|
||||
|
||||
function renderRevealResponse(response: SecretRevealResponseDto): void {
|
||||
const input = document.querySelector<HTMLInputElement>("#secretRevealValue");
|
||||
const absent = document.querySelector<HTMLElement>("#secretRevealValueAbsent");
|
||||
const result = document.querySelector<HTMLElement>("#secretRevealResultPanel");
|
||||
const confirmation = document.querySelector<HTMLElement>("#secretRevealConfirmPanel");
|
||||
const confirm = document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction");
|
||||
const toggle = document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility");
|
||||
if (confirmation) {
|
||||
confirmation.hidden = true;
|
||||
}
|
||||
if (confirm) {
|
||||
confirm.hidden = true;
|
||||
}
|
||||
if (result) {
|
||||
result.hidden = false;
|
||||
}
|
||||
if (response.value === null) {
|
||||
if (input) {
|
||||
input.value = "";
|
||||
input.hidden = true;
|
||||
}
|
||||
if (absent) {
|
||||
absent.hidden = false;
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.hidden = true;
|
||||
}
|
||||
} else {
|
||||
if (input) {
|
||||
input.hidden = false;
|
||||
input.type = "password";
|
||||
input.value = response.value;
|
||||
}
|
||||
if (absent) {
|
||||
absent.hidden = true;
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.hidden = false;
|
||||
toggle.textContent = "Afficher";
|
||||
}
|
||||
}
|
||||
frontendDebug("main", "Privileged Secret reveal rendered", {
|
||||
variableName: response.variableName,
|
||||
source: response.source,
|
||||
valuePresent: response.value !== null,
|
||||
});
|
||||
}
|
||||
|
||||
async function executeReveal(): Promise<void> {
|
||||
if (!pendingRequest || revealInFlight) {
|
||||
return;
|
||||
}
|
||||
const request = pendingRequest;
|
||||
const requestGeneration = revealGeneration;
|
||||
const confirm = document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction");
|
||||
const status = document.querySelector<HTMLElement>("#secretRevealStatus");
|
||||
revealInFlight = true;
|
||||
if (confirm) {
|
||||
confirm.disabled = true;
|
||||
}
|
||||
if (status) {
|
||||
status.textContent = "Reveal privilégié en cours via ksp-config-lib...";
|
||||
}
|
||||
frontendDebug("main", "Privileged Secret reveal requested", { variableName: request.variableName, source: request.source });
|
||||
try {
|
||||
const response = await invokeKsp<SecretRevealResponseDto>("main", "reveal_environment_value", { request });
|
||||
if (requestGeneration !== revealGeneration || !pendingRequest) {
|
||||
frontendDebug("main", "Privileged Secret reveal response discarded after modal state changed", {
|
||||
variableName: request.variableName,
|
||||
source: request.source,
|
||||
});
|
||||
return;
|
||||
}
|
||||
renderRevealResponse(response);
|
||||
if (status) {
|
||||
status.textContent = response.value === null ? "Aucune valeur n'existe dans la source demandée." : "Valeur révélée temporairement. Elle sera effacée à la fermeture.";
|
||||
}
|
||||
} catch {
|
||||
if (status) {
|
||||
status.textContent = "Le reveal a été refusé ou a échoué.";
|
||||
}
|
||||
if (confirm) {
|
||||
confirm.disabled = false;
|
||||
}
|
||||
} finally {
|
||||
if (requestGeneration === revealGeneration) {
|
||||
revealInFlight = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRevealVisibility(): void {
|
||||
const input = document.querySelector<HTMLInputElement>("#secretRevealValue");
|
||||
const toggle = document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility");
|
||||
if (!input || !toggle || input.hidden) {
|
||||
return;
|
||||
}
|
||||
const show = input.type === "password";
|
||||
input.type = show ? "text" : "password";
|
||||
toggle.textContent = show ? "Masquer" : "Afficher";
|
||||
frontendDebug("main", "Transient Secret reveal visibility changed", { visible: show });
|
||||
}
|
||||
|
||||
export function requestSecretReveal(variableName: string, source: SecretRevealSource): void {
|
||||
const modal = modalElement();
|
||||
const variable = document.querySelector<HTMLElement>("#secretRevealVariable");
|
||||
const sourceElement = document.querySelector<HTMLElement>("#secretRevealSource");
|
||||
const status = document.querySelector<HTMLElement>("#secretRevealStatus");
|
||||
if (!modal || !variable || !sourceElement || !status) {
|
||||
frontendDebug("main", "Privileged Secret reveal modal is unavailable", { variableName, source });
|
||||
return;
|
||||
}
|
||||
revealGeneration += 1;
|
||||
clearSecretValue();
|
||||
pendingRequest = { variableName, source };
|
||||
variable.textContent = variableName;
|
||||
sourceElement.textContent = sourceLabel(source);
|
||||
status.textContent = "Confirmation explicite requise avant tout accès à la valeur réelle.";
|
||||
frontendDebug("main", "Privileged Secret reveal confirmation opened", { variableName, source });
|
||||
Modal.getOrCreateInstance(modal).show();
|
||||
}
|
||||
|
||||
export function clearTransientSecretReveal(): void {
|
||||
const modal = modalElement();
|
||||
const variableName = pendingRequest?.variableName ?? null;
|
||||
revealGeneration += 1;
|
||||
clearSecretValue();
|
||||
pendingRequest = null;
|
||||
revealInFlight = false;
|
||||
if (modal) {
|
||||
Modal.getOrCreateInstance(modal).hide();
|
||||
}
|
||||
frontendTrace("main", "Transient Secret reveal state cleared", { variableName });
|
||||
}
|
||||
|
||||
export function initializeSecretReveal(): void {
|
||||
const modal = modalElement();
|
||||
document.querySelector<HTMLButtonElement>("#secretRevealConfirmAction")?.addEventListener("click", () => {
|
||||
void executeReveal();
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#secretRevealToggleVisibility")?.addEventListener("click", () => {
|
||||
toggleRevealVisibility();
|
||||
});
|
||||
modal?.addEventListener("hidden.bs.modal", () => {
|
||||
const variableName = pendingRequest?.variableName ?? null;
|
||||
revealGeneration += 1;
|
||||
clearSecretValue();
|
||||
pendingRequest = null;
|
||||
revealInFlight = false;
|
||||
frontendDebug("main", "Privileged Secret reveal closed and value cleared", { variableName });
|
||||
});
|
||||
frontendTrace("main", "Privileged Secret reveal handlers installed");
|
||||
}
|
||||
Reference in New Issue
Block a user