mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
feat(hk): reveal-and-copy card for reset password (+ catalog cleanup)
Two things in one commit because they sit on top of each other:
1. **Reset password reveal card.** The emulator's
`HousekeepingResetUserPasswordEvent` already returns the freshly
generated 12-char plaintext in the action-result `message`, but
the client was leaking it through the standard success-banner
pipeline — auto-dismiss in 4s, truncated, no copy button. Operators
were missing it.
- New `<HousekeepingPasswordReveal />` card mounted in the panel
header (between the status banner and tab content). Stays put
until manually dismissed.
- `useHousekeepingStore` gains a dedicated `passwordReveal` slot
(`{ userId, username, password }`) plus `revealPassword()` /
`clearPasswordReveal()` setters. Sensitive data, kept OUT of the
generic banner / toast pipeline.
- `useHousekeepingActions.resetUserPassword` no longer routes
through `wrap()` — it intercepts the result, lifts the
plaintext into the reveal slot, and uses a localizable success
key (`housekeeping.action.reset_password.done`) for the banner so
the password itself never lands there.
- Copy button uses `navigator.clipboard.writeText` in secure
contexts with a `document.execCommand('copy')` fallback for
http:// deployments. Confirmation icon flips to a checkmark for
~1.6s on success. The input is `select-all` + auto-select on
focus so Ctrl+C is also a manual fallback.
- 8 new i18n keys (EN + IT, .example + runtime UITexts.json5 /
UITexts.en.json5).
2. **Catalog admin cleanup ported from the PR branch.** The dev
branch was still carrying the catalog admin code (handlers, hooks,
store slots, i18n keys) even though the local renderer is on the
catalog-stripped `feat/housekeeping-packets` branch — typecheck
was breaking because the catalog composers no longer exist on the
linked renderer. Stripped here to match: 4 catalog actions
removed from `HousekeepingActionType`, `HousekeepingApi.ts`,
`useHousekeepingActions`, `useHousekeepingStore`. The CATALOG tab
id is gone from `HousekeepingTabId`. Catalog interfaces
(`IHousekeepingCatalogPage` / `IHousekeepingCatalogOffer`) are
dropped. 17 catalog i18n keys removed per locale. Two test files
updated to match.
This commit is contained in:
@@ -94,7 +94,7 @@ const validationOr = (key: HousekeepingErrorKey, markDone: (e: string | null, s:
|
||||
*/
|
||||
export const useHousekeepingActions = () =>
|
||||
{
|
||||
const { selectedUser, selectedRoom, markActionPending, markActionDone, setSelectedUser, setSelectedRoom, recordActionMetric } = useHousekeepingStore();
|
||||
const { selectedUser, selectedRoom, markActionPending, markActionDone, setSelectedUser, setSelectedRoom, recordActionMetric, revealPassword } = useHousekeepingStore();
|
||||
const { showSingleBubble } = useNotification();
|
||||
// Stable closure-bound runner so every action below stays a
|
||||
// one-liner: only the runner thunk + a per-action telemetry
|
||||
@@ -151,8 +151,53 @@ export const useHousekeepingActions = () =>
|
||||
{
|
||||
if(!validationOr(validatePositiveId(userId, 'user'), markActionDone)) return null;
|
||||
|
||||
return runAction(() => HousekeepingApi.resetUserPassword(userId), 'resetUserPassword');
|
||||
}, [ runAction, markActionDone ]);
|
||||
// Run the action with a localizable success message — we
|
||||
// INTERCEPT before `wrap`'s default behavior leaks the plaintext
|
||||
// into the auto-dismissing status banner. The emulator returns
|
||||
// the freshly-generated plaintext in `result.message`; we lift it
|
||||
// into the dedicated `passwordReveal` slot which renders a
|
||||
// persistent card with a copy button. The wrapping `runAction`
|
||||
// would also fire a transient toast with whatever string lands
|
||||
// in `message`, so we bypass it via a direct API call + manual
|
||||
// status writes here.
|
||||
const username = (selectedUser && selectedUser.id === userId) ? selectedUser.username : '';
|
||||
|
||||
markActionPending();
|
||||
const startedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
const measure = (isError: boolean) =>
|
||||
{
|
||||
const endedAt = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
||||
recordActionMetric('resetUserPassword', endedAt - startedAt, isError);
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
const result = await HousekeepingApi.resetUserPassword(userId);
|
||||
|
||||
if(!result || result.ok === false)
|
||||
{
|
||||
markActionDone(result?.message || 'housekeeping.action.error', null);
|
||||
measure(true);
|
||||
return result ?? null;
|
||||
}
|
||||
|
||||
const plaintext = result.message ?? '';
|
||||
|
||||
if(plaintext) revealPassword(userId, username, plaintext);
|
||||
|
||||
// Generic success key — does NOT include the plaintext, so
|
||||
// even if the banner is visible the password isn't in it.
|
||||
markActionDone(null, 'housekeeping.action.reset_password.done');
|
||||
measure(false);
|
||||
return result;
|
||||
}
|
||||
catch(error)
|
||||
{
|
||||
markActionDone(String((error as Error)?.message ?? error), null);
|
||||
measure(true);
|
||||
return null;
|
||||
}
|
||||
}, [ markActionPending, markActionDone, selectedUser, revealPassword, recordActionMetric ]);
|
||||
|
||||
const setUserRank = useCallback(async (userId: number, rank: number) =>
|
||||
{
|
||||
|
||||
@@ -42,6 +42,19 @@ const useHousekeepingStoreInner = () =>
|
||||
// on referential equality — mutating a Set in place would miss
|
||||
// updates. Capped via the dedupe in toggleUserSelection.
|
||||
const [ selectedUserIds, setSelectedUserIds ] = useState<number[]>([]);
|
||||
// Password-reveal state — when reset-password succeeds, the emulator
|
||||
// returns the freshly-generated plaintext password in the action
|
||||
// result. We hold it in a dedicated state slot (not the success
|
||||
// banner) so it doesn't auto-dismiss and the operator can read /
|
||||
// copy it. Cleared manually via `clearPasswordReveal()` — sensitive
|
||||
// data, treat it like a one-shot secret.
|
||||
const [ passwordReveal, setPasswordReveal ] = useState<{ userId: number; username: string; password: string } | null>(null);
|
||||
const revealPassword = useCallback((userId: number, username: string, password: string) =>
|
||||
{
|
||||
if(!password) return;
|
||||
setPasswordReveal({ userId, username, password });
|
||||
}, []);
|
||||
const clearPasswordReveal = useCallback(() => setPasswordReveal(null), []);
|
||||
// Per-action latency / count / error metrics. Map → triggers a
|
||||
// new reference on every update so subscribers re-render.
|
||||
// Capped per-action via `recordSample`'s sliding window so the
|
||||
@@ -497,6 +510,9 @@ const useHousekeepingStoreInner = () =>
|
||||
selectedUserIds,
|
||||
toggleUserSelection,
|
||||
clearUserSelection,
|
||||
passwordReveal,
|
||||
revealPassword,
|
||||
clearPasswordReveal,
|
||||
metricsByAction,
|
||||
recordActionMetric,
|
||||
resetActionMetrics
|
||||
|
||||
Reference in New Issue
Block a user