v0.1.4-pre.013
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
|
||||
<!-- version: 8 -->
|
||||
<!-- version: 9 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -265,6 +265,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
|
||||
<span class="small text-body-secondary me-1">Reveal Secret privilégié :</span>
|
||||
<button id="revealEnvironmentEffective" class="btn btn-outline-warning btn-sm" type="button" disabled>Révéler effective</button>
|
||||
<button id="revealEnvironmentDotenv" class="btn btn-outline-warning btn-sm" type="button" disabled>Révéler .env</button>
|
||||
<span class="form-text mb-0">Disponible uniquement pour KSP_SECRET_* / KSPB_SECRET_* et jamais préchargé automatiquement.</span>
|
||||
</div>
|
||||
|
||||
<div id="environmentMutationStatus" class="alert alert-secondary mb-3" role="status" aria-live="polite">
|
||||
Aucune mutation effectuée pendant ce lancement.
|
||||
</div>
|
||||
@@ -325,6 +332,39 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="secretRevealModal" class="modal fade" tabindex="-1" aria-labelledby="secretRevealModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 id="secretRevealModalLabel" class="modal-title fs-5">Reveal Secret privilégié</h2>
|
||||
<button class="btn-close" type="button" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="secretRevealConfirmPanel">
|
||||
<p class="mb-2">Variable : <code id="secretRevealVariable" class="font-monospace"></code></p>
|
||||
<p class="mb-3">Source demandée : <code id="secretRevealSource"></code></p>
|
||||
<div class="alert alert-warning small mb-3" role="note">
|
||||
Cette action expose temporairement la valeur réelle dans la WebView. Elle n'est ni journalisée, ni ajoutée à AppState, ni persistée par le frontend.
|
||||
</div>
|
||||
</div>
|
||||
<div id="secretRevealResultPanel" hidden>
|
||||
<label class="form-label fw-semibold" for="secretRevealValue">Valeur réelle transitoire</label>
|
||||
<div class="input-group">
|
||||
<input id="secretRevealValue" class="form-control font-monospace" type="password" readonly autocomplete="off" spellcheck="false">
|
||||
<button id="secretRevealToggleVisibility" class="btn btn-outline-secondary" type="button">Afficher</button>
|
||||
</div>
|
||||
<div id="secretRevealValueAbsent" class="alert alert-secondary small mb-0" hidden>Aucune valeur n'existe dans cette source.</div>
|
||||
</div>
|
||||
<p id="secretRevealStatus" class="text-body-secondary small mt-3 mb-0" aria-live="polite"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-outline-secondary" type="button" data-bs-dismiss="modal">Fermer</button>
|
||||
<button id="secretRevealConfirmAction" class="btn btn-warning" type="button">Révéler</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="app-footer bg-dark text-light">
|
||||
<div class="container h-100 d-flex align-items-center justify-content-center">
|
||||
<small>© 2026 SASEDEV</small>
|
||||
|
||||
@@ -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