R-193: the recovery screen — unlocking, and only unlocking (v0.200.0)
A customer whose machine was rebuilt had everything needed to get their data back and no way to find out: the only route was a command line. This is the screen that closes that. IT UNLOCKS, AND ONLY UNLOCKS (operator ruling). It explains, takes the recovery code, opens the repository and shows what is in there — apps, dates, sizes. It restores nothing: restore is already per-app and lives in the backups area, and a screen that unlocks and then offers to overwrite is two decisions wearing one button. ONE CORE, TWO CALLERS. RecoverInstallCore is split out of RecoverAndInstall; the CLI wrapper keeps its exit codes and printed lines byte-identical, and the handler drives the same function. Two implementations of the one operation that can permanently lose a customer's data would drift, and only one would be tested. Asserted from source on both sides by AST. THREE WAYS OUT, none a dismiss button: recover; 'most nem' (the full page stops interrupting, the backups-area entry point stays PERMANENTLY, bound to the offer and never to the postpone flag); and 'I do not want the old data' — confirmed TWICE and reaching the SHIPPED move-aside, which sets aside and never deletes. THE CODE IS HANDLED NO MORE LOOSELY THAN ON THE COMMAND LINE: POST body only, never logged, never persisted, never echoed, cleared on every path, no-store, autocomplete off. No lockout — the code is a ten-word phrase, and locking a customer out of their own data for a typo is worse than anything it prevents. TWO DEFECTS THE TESTS CAUGHT, both fixed: an UNCLAIMED (legacy-open) box would have been shown the page, because RequireAuth passes such a box through; and the inventory nil-dereferenced when no off-site target was configured, which is exactly the pristine rebuilt shape.
This commit is contained in:
@@ -96,6 +96,15 @@ type Server struct {
|
||||
escrowStageFn func(ctx context.Context) error
|
||||
escrowStaleFn func() bool
|
||||
|
||||
// escrowSealedAtFn (v0.200.0, R-193) reports WHEN the hub's sealed recovery package was created —
|
||||
// the one non-secret fact the recovery screen may state before a code is entered. Wired via
|
||||
// SetEscrowSealedAt from the report ACK; nil → the page says nothing about the date rather than
|
||||
// guessing one.
|
||||
escrowSealedAtFn func() string
|
||||
// recoveryRecovererFn is the recovery screen's agent seam (nil → the shared agentClient(), the
|
||||
// same channel the CLI uses). Tests inject a fake so the HANDLER itself can be driven.
|
||||
recoveryRecovererFn func() (backup.OffsiteKeyRecoverer, error)
|
||||
|
||||
// NAS add orchestration (verify-before-commit): the single-flight job slot + the two seams.
|
||||
// netAgentFn nil → the shared agentClient(); netProbeFn nil → runNetProbe (the uid-1000 re-exec).
|
||||
netAdd netAddState
|
||||
@@ -361,6 +370,15 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.logger.Printf("[DEBUG] [web] ServeHTTP: %s %s from %s", r.Method, logPath, r.RemoteAddr)
|
||||
}
|
||||
|
||||
// R-193: the recovery screen takes over the LANDING pages (and only those) while the situation
|
||||
// holds and the customer has not postponed. Placed before the switch so it cannot be defeated by
|
||||
// a route added later, and scoped to two paths so it never traps the customer inside it — every
|
||||
// other page, including the backups area the entry point lives in, stays reachable.
|
||||
if (path == "/launcher" || path == "/dashboard") && r.Method == http.MethodGet && s.recoveryInterrupts() {
|
||||
http.Redirect(w, r, "/recovery", http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
// Customer-claim arc (v0.122.0, F-4): the code-entry page + its handlers. Reachable pre-auth
|
||||
// (code-gated internally); CSRF via the pre-auth HMAC token (validated inside the handlers).
|
||||
@@ -375,6 +393,16 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// canonical landing page. "/" 302s to /launcher (ONE canonical URL per page — the launcher body
|
||||
// is never served AT "/"). Post-login lands on "/", so it flows here → the launcher.
|
||||
http.Redirect(w, r, "/launcher", http.StatusFound)
|
||||
// R-193 — the recovery screen. A FULL PAGE, not a banner: someone who has just lost a machine
|
||||
// deserves a screen about that and nothing else. It takes over the landing pages while the
|
||||
// situation holds AND the customer has not chosen "most nem"; afterwards it stays reachable here
|
||||
// (and from the backups area) for as long as the situation lasts.
|
||||
case path == "/recovery" && r.Method == http.MethodGet:
|
||||
s.recoveryPageHandler(w, r)
|
||||
case path == "/recovery/unlock" && r.Method == http.MethodPost:
|
||||
s.recoveryUnlockHandler(w, r)
|
||||
case path == "/recovery/postpone" && r.Method == http.MethodPost:
|
||||
s.recoveryPostponeHandler(w, r)
|
||||
case path == "/dashboard":
|
||||
s.dashboardHandler(w, r)
|
||||
case path == "/launcher":
|
||||
|
||||
Reference in New Issue
Block a user