v0.2.6-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-app-wallet-desk/frontend/ts/main.ts
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
import "bootstrap";
|
||||
import DataTable from "datatables.net-bs5";
|
||||
@@ -14,6 +14,7 @@ import type { WalletSelectionRequestDto } from "./bindings/ksp_app_wallet_desk/w
|
||||
import type { WalletAuthorizedDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletAuthorizedDto.ts";
|
||||
import type { WalletCreateRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletCreateRequestDto.ts";
|
||||
import type { WalletSessionDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletSessionDto.ts";
|
||||
import type { WalletUnlockRequestDto } from "./bindings/ksp_app_wallet_desk/wallet_session/WalletUnlockRequestDto.ts";
|
||||
import { frontendDebug, frontendInfo, frontendTrace, frontendWarn, installFrontendConsoleBridge } from "./frontend_log";
|
||||
import { invokeKsp } from "./invoke";
|
||||
import "../sass/main.scss";
|
||||
@@ -22,7 +23,8 @@ import "../sass/main.scss";
|
||||
installFrontendConsoleBridge("main");
|
||||
|
||||
type ViewId = "dashboard" | "wallets" | "create-import" | "details" | "security" | "diagnostics";
|
||||
type ActiveSessionState = "no_selection" | "locked" | "owner_open";
|
||||
type ActiveSessionState = "no_selection" | "locked" | "privileged_operation" | "view_open" | "owner_open";
|
||||
type UnlockCapability = "view" | "owner";
|
||||
|
||||
const viewTitles: Record<ViewId, string> = {
|
||||
dashboard: "Dashboard",
|
||||
@@ -35,6 +37,8 @@ const viewTitles: Record<ViewId, string> = {
|
||||
|
||||
let activeWalletId: string | null = null;
|
||||
let activeSessionState: ActiveSessionState = "no_selection";
|
||||
let activeViewEnabled = false;
|
||||
let configuredSecretCandidateCount = 0;
|
||||
|
||||
function isViewId(value: string): value is ViewId {
|
||||
return value in viewTitles;
|
||||
@@ -121,8 +125,8 @@ function renderWalletInventoryRow(entry: WalletInventoryEntryDto): HTMLTableRowE
|
||||
row.dataset.inspectionStatus = entry.inspectionStatus;
|
||||
const state = document.createElement("td");
|
||||
if (entry.state === "locked") {
|
||||
if (activeWalletId === entry.walletId && activeSessionState === "owner_open") {
|
||||
appendIconText(state, "fa-lock-open", "OWNER");
|
||||
if (activeWalletId === entry.walletId && (activeSessionState === "view_open" || activeSessionState === "owner_open")) {
|
||||
appendIconText(state, "fa-lock-open", activeSessionState === "view_open" ? "VIEW" : "OWNER");
|
||||
} else {
|
||||
appendIconText(state, "fa-lock", "Locked");
|
||||
}
|
||||
@@ -165,7 +169,7 @@ function initializeWalletTable(): void {
|
||||
zeroRecords: "Aucun wallet correspondant.",
|
||||
},
|
||||
});
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.005-wallet-session-create" });
|
||||
frontendDebug("main", "Wallet inventory DataTable initialized", { phase: "pre.006-wallet-unlock" });
|
||||
}
|
||||
|
||||
function renderWalletInventory(entries: WalletInventoryEntryDto[]): void {
|
||||
@@ -192,11 +196,33 @@ function updateSessionActions(): void {
|
||||
const lock = document.querySelector<HTMLButtonElement>('[data-shell-action="lock"]');
|
||||
const deselect = document.querySelector<HTMLButtonElement>('[data-shell-action="deselect"]');
|
||||
if (lock) {
|
||||
lock.disabled = activeSessionState !== "owner_open";
|
||||
lock.disabled = activeSessionState !== "view_open" && activeSessionState !== "owner_open";
|
||||
}
|
||||
if (deselect) {
|
||||
deselect.disabled = activeSessionState === "no_selection";
|
||||
deselect.disabled = activeSessionState === "no_selection" || activeSessionState === "privileged_operation";
|
||||
}
|
||||
updateUnlockActions();
|
||||
}
|
||||
|
||||
function updateUnlockActions(): void {
|
||||
const locked = activeSessionState === "locked";
|
||||
const viewManual = document.querySelector<HTMLButtonElement>("#unlockViewManual");
|
||||
const ownerManual = document.querySelector<HTMLButtonElement>("#unlockOwnerManual");
|
||||
const viewConfigured = document.querySelector<HTMLButtonElement>("#unlockViewConfigured");
|
||||
const ownerConfigured = document.querySelector<HTMLButtonElement>("#unlockOwnerConfigured");
|
||||
if (viewManual) {
|
||||
viewManual.disabled = !locked || !activeViewEnabled;
|
||||
}
|
||||
if (ownerManual) {
|
||||
ownerManual.disabled = !locked;
|
||||
}
|
||||
if (viewConfigured) {
|
||||
viewConfigured.disabled = !locked || !activeViewEnabled || configuredSecretCandidateCount === 0;
|
||||
}
|
||||
if (ownerConfigured) {
|
||||
ownerConfigured.disabled = !locked || configuredSecretCandidateCount === 0;
|
||||
}
|
||||
setText("#unlockSecretCandidateCount", configuredSecretCandidateCount.toString());
|
||||
}
|
||||
|
||||
function clearCreateFormSensitiveInputs(): void {
|
||||
@@ -218,16 +244,30 @@ function clearCreateFormSensitiveInputs(): void {
|
||||
}
|
||||
}
|
||||
|
||||
function clearUnlockSensitiveInputs(): void {
|
||||
const viewPassword = document.querySelector<HTMLInputElement>("#unlockWalletViewPassword");
|
||||
const ownerPassword = document.querySelector<HTMLInputElement>("#unlockWalletOwnerPassword");
|
||||
if (viewPassword) {
|
||||
viewPassword.value = "";
|
||||
}
|
||||
if (ownerPassword) {
|
||||
ownerPassword.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
function clearAuthorizedProjection(): void {
|
||||
setText("#currentWalletPubkey", "—");
|
||||
setText("#currentWalletAlias", "—");
|
||||
setText("#currentWalletNotes", "—");
|
||||
clearCreateFormSensitiveInputs();
|
||||
clearUnlockSensitiveInputs();
|
||||
}
|
||||
|
||||
function clearSelectedWallet(): void {
|
||||
activeWalletId = null;
|
||||
activeSessionState = "no_selection";
|
||||
activeViewEnabled = false;
|
||||
configuredSecretCandidateCount = 0;
|
||||
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
|
||||
if (icon) {
|
||||
icon.className = "fa-solid fa-lock fa-2x mb-3 text-body-secondary";
|
||||
@@ -236,6 +276,8 @@ function clearSelectedWallet(): void {
|
||||
setText("#currentWalletState", "—");
|
||||
setText("#currentWalletFormat", "—");
|
||||
setText("#currentWalletView", "—");
|
||||
setText("#currentWalletSecretCandidates", "0");
|
||||
setText("#unlockStatus", "Sélectionner un wallet verrouillé.");
|
||||
clearAuthorizedProjection();
|
||||
updateSessionActions();
|
||||
}
|
||||
@@ -243,6 +285,8 @@ function clearSelectedWallet(): void {
|
||||
function renderLockedWallet(wallet: LockedWalletDto): void {
|
||||
activeWalletId = wallet.walletId;
|
||||
activeSessionState = "locked";
|
||||
activeViewEnabled = wallet.viewEnabled;
|
||||
configuredSecretCandidateCount = wallet.configuredSecretCandidateCount;
|
||||
clearAuthorizedProjection();
|
||||
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
|
||||
if (icon) {
|
||||
@@ -252,26 +296,45 @@ function renderLockedWallet(wallet: LockedWalletDto): void {
|
||||
setText("#currentWalletState", "Locked");
|
||||
setText("#currentWalletFormat", wallet.formatVersion.toString());
|
||||
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
|
||||
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
|
||||
setText("#unlockStatus", "Prêt pour une tentative explicite. Argon2 peut prendre plusieurs secondes.");
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "Locked Wallet selection rendered", { walletId: wallet.walletId, formatVersion: wallet.formatVersion, viewEnabled: wallet.viewEnabled });
|
||||
frontendDebug("main", "Locked Wallet selection rendered", {
|
||||
configuredSecretCandidateCount,
|
||||
formatVersion: wallet.formatVersion,
|
||||
viewEnabled: wallet.viewEnabled,
|
||||
walletId: wallet.walletId,
|
||||
});
|
||||
}
|
||||
|
||||
function renderAuthorizedWallet(wallet: WalletAuthorizedDto): void {
|
||||
activeWalletId = wallet.walletId;
|
||||
activeSessionState = "owner_open";
|
||||
activeSessionState = wallet.capability === "view" ? "view_open" : "owner_open";
|
||||
activeViewEnabled = wallet.viewEnabled;
|
||||
configuredSecretCandidateCount = wallet.configuredSecretCandidateCount;
|
||||
clearUnlockSensitiveInputs();
|
||||
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
|
||||
if (icon) {
|
||||
icon.className = "fa-solid fa-lock-open fa-2x mb-3 text-success";
|
||||
}
|
||||
const capabilityLabel = wallet.capability === "view" ? "VIEW" : "OWNER";
|
||||
setText("#currentWalletFilename", wallet.filename);
|
||||
setText("#currentWalletState", "OWNER open");
|
||||
setText("#currentWalletState", `${capabilityLabel} open`);
|
||||
setText("#currentWalletFormat", wallet.formatVersion.toString());
|
||||
setText("#currentWalletView", wallet.viewEnabled ? "enabled" : "disabled");
|
||||
setText("#currentWalletSecretCandidates", configuredSecretCandidateCount.toString());
|
||||
setText("#currentWalletPubkey", wallet.pubkey);
|
||||
setText("#currentWalletAlias", wallet.alias ?? "—");
|
||||
setText("#currentWalletNotes", wallet.notes.length === 0 ? "—" : wallet.notes.map(note => note.text).join(" · "));
|
||||
setText("#unlockStatus", `${capabilityLabel} ouvert. Lock pour purger le handle autorisé.`);
|
||||
updateSessionActions();
|
||||
frontendDebug("main", "OWNER Wallet session rendered after creation", { walletId: wallet.walletId, viewEnabled: wallet.viewEnabled, noteCount: wallet.notes.length });
|
||||
frontendDebug("main", "Authorized Wallet session rendered", {
|
||||
capability: wallet.capability,
|
||||
configuredSecretCandidateCount,
|
||||
noteCount: wallet.notes.length,
|
||||
viewEnabled: wallet.viewEnabled,
|
||||
walletId: wallet.walletId,
|
||||
});
|
||||
}
|
||||
|
||||
async function selectWallet(walletId: string): Promise<void> {
|
||||
@@ -395,6 +458,94 @@ function bindCreateWalletForm(): void {
|
||||
frontendTrace("main", "Create Wallet form handlers installed");
|
||||
}
|
||||
|
||||
function beginFrontendUnlock(capability: UnlockCapability, provider: "manual" | "configured"): string | null {
|
||||
if (activeSessionState !== "locked" || !activeWalletId) {
|
||||
return null;
|
||||
}
|
||||
const walletId = activeWalletId;
|
||||
activeSessionState = "privileged_operation";
|
||||
setText("#currentWalletState", `Unlock ${capability.toUpperCase()}…`);
|
||||
setText("#unlockStatus", provider === "manual" ? "Dérivation Argon2 en cours…" : `Tentative explicite sur ${configuredSecretCandidateCount} secret(s) configuré(s)…`);
|
||||
updateSessionActions();
|
||||
return walletId;
|
||||
}
|
||||
|
||||
function restoreFrontendLockedAfterUnlockFailure(walletId: string): void {
|
||||
if (activeWalletId !== walletId) {
|
||||
return;
|
||||
}
|
||||
activeSessionState = "locked";
|
||||
clearAuthorizedProjection();
|
||||
const icon = document.querySelector<HTMLElement>("#currentWalletIcon");
|
||||
if (icon) {
|
||||
icon.className = "fa-solid fa-lock fa-2x mb-3";
|
||||
}
|
||||
setText("#currentWalletState", "Locked");
|
||||
setText("#unlockStatus", "Unlock refusé. Le wallet reste verrouillé.");
|
||||
updateSessionActions();
|
||||
}
|
||||
|
||||
async function unlockManual(capability: UnlockCapability): Promise<void> {
|
||||
const input = document.querySelector<HTMLInputElement>(capability === "view" ? "#unlockWalletViewPassword" : "#unlockWalletOwnerPassword");
|
||||
if (!input || input.value.length === 0) {
|
||||
setText("#unlockStatus", "Password manuel requis.");
|
||||
return;
|
||||
}
|
||||
const request: WalletUnlockRequestDto = { password: input.value };
|
||||
const walletId = beginFrontendUnlock(capability, "manual");
|
||||
if (!walletId) {
|
||||
clearUnlockSensitiveInputs();
|
||||
return;
|
||||
}
|
||||
const command = capability === "view" ? "unlock_wallet_view_manual" : "unlock_wallet_owner_manual";
|
||||
frontendDebug("main", "Manual Wallet unlock requested", { capability, walletId });
|
||||
try {
|
||||
const wallet = await invokeKsp<WalletAuthorizedDto>("main", command, { request });
|
||||
clearUnlockSensitiveInputs();
|
||||
renderAuthorizedWallet(wallet);
|
||||
await loadWalletInventory("list_wallets", false);
|
||||
activateView("dashboard", "user");
|
||||
} catch {
|
||||
clearUnlockSensitiveInputs();
|
||||
restoreFrontendLockedAfterUnlockFailure(walletId);
|
||||
frontendWarn("main", "Manual Wallet unlock failed", { capability, walletId });
|
||||
}
|
||||
}
|
||||
|
||||
async function unlockConfigured(capability: UnlockCapability): Promise<void> {
|
||||
const walletId = beginFrontendUnlock(capability, "configured");
|
||||
if (!walletId) {
|
||||
return;
|
||||
}
|
||||
const command = capability === "view" ? "unlock_wallet_view_configured" : "unlock_wallet_owner_configured";
|
||||
frontendDebug("main", "Configured-secret Wallet unlock requested", { capability, configuredSecretCandidateCount, walletId });
|
||||
try {
|
||||
const wallet = await invokeKsp<WalletAuthorizedDto>("main", command);
|
||||
renderAuthorizedWallet(wallet);
|
||||
await loadWalletInventory("list_wallets", false);
|
||||
activateView("dashboard", "user");
|
||||
} catch {
|
||||
restoreFrontendLockedAfterUnlockFailure(walletId);
|
||||
frontendWarn("main", "Configured-secret Wallet unlock failed", { capability, configuredSecretCandidateCount, walletId });
|
||||
}
|
||||
}
|
||||
|
||||
function bindUnlockActions(): void {
|
||||
document.querySelector<HTMLButtonElement>("#unlockViewManual")?.addEventListener("click", () => {
|
||||
void unlockManual("view");
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#unlockOwnerManual")?.addEventListener("click", () => {
|
||||
void unlockManual("owner");
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#unlockViewConfigured")?.addEventListener("click", () => {
|
||||
void unlockConfigured("view");
|
||||
});
|
||||
document.querySelector<HTMLButtonElement>("#unlockOwnerConfigured")?.addEventListener("click", () => {
|
||||
void unlockConfigured("owner");
|
||||
});
|
||||
frontendTrace("main", "Wallet unlock action handlers installed");
|
||||
}
|
||||
|
||||
async function lockCurrentWallet(): Promise<void> {
|
||||
clearAuthorizedProjection();
|
||||
try {
|
||||
@@ -431,7 +582,7 @@ function renderRuntimeStatus(status: RuntimeStatusDto): void {
|
||||
setText("#runtimeEffectiveWalletsDirectory", status.effectiveWalletsDirectory);
|
||||
setText("#runtimeWalletDirectoryCreated", status.effectiveWalletsDirectoryCreatedOnStartup ? "oui" : "non, déjà présent");
|
||||
setText("#runtimeShellPhase", status.shellPhase);
|
||||
setText("#shellStatus", "Config résolue ; création et session Wallet prêtes.");
|
||||
setText("#shellStatus", "Config résolue ; création et unlock VIEW/OWNER prêts.");
|
||||
frontendTrace("main", "Wallet Desk runtime status rendered", {
|
||||
compositeProfile: status.activeCompositeProfile,
|
||||
fallbackLoggingActive: status.fallbackLoggingActive,
|
||||
@@ -477,6 +628,7 @@ async function initializeMain(): Promise<void> {
|
||||
bindNavigation();
|
||||
bindWalletTableSelection();
|
||||
bindCreateWalletForm();
|
||||
bindUnlockActions();
|
||||
bindShellActions();
|
||||
clearSelectedWallet();
|
||||
activateView("dashboard", "startup");
|
||||
|
||||
Reference in New Issue
Block a user