R-729 + R-545: the household can remove its own off-site target („Távoli mentési cél törlése")

POST /backup/offbox/clear (reveal-then-confirm) forgets the target and deletes ssh_key + known_hosts;
nothing on the target is touched. repo_password is kept whenever anything could depend on it (hub
sealed package — the R-241 rule; escrowed; a successful run; snapshots) and deleted only otherwise.
Refused for the Felhom tier (rclone-pinned), while the single-flight is held, and while an
abandonment countdown runs. i18n parity fixtures of backups_remote gain the additive block only.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 21:24:28 +02:00
parent 6f1ba1fe43
commit 27b373b93c
23 changed files with 612 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
package backup
import (
"errors"
"fmt"
"os"
)
// CLEARING AN OFF-SITE TARGET — R-729 / R-545 (the household's „Távoli mentési cél törlése" press).
//
// Before this, a target once saved could be edited or switched off but never removed: the page
// refuses an empty address, and the only other route (`/backup/offbox/reset`) means "start a new
// remote backup, set the old history aside", and only for an ORPHANED store. A household that tried
// its own NAS and gave up kept the host, user, path, SSH key and known-host line for ever.
//
// WHAT THE PRESS DOES: forgets the target (settings) and deletes the transport secrets (`ssh_key`,
// `known_hosts`) from `data/offbox/`. It NEVER touches the repository on the target — not one remote
// command runs (R-729: "never the repository").
//
// WHAT IT DOES WITH THE REPOSITORY PASSWORD — the one secret that is not transport:
// it is KEPT whenever anything could depend on it, and deleted only when nothing can:
// - the hub holds a sealed recovery package for this box (the R-241 rule: a box without a password
// and with a held package refuses to mint, so deleting the key here would strand the next target
// in the „awaiting recovery key" holding state — and the history the package protects would lose
// its on-box key);
// - the escrow was confirmed (`escrowed`) — the hub's package seals exactly this key;
// - the target ever completed a run or holds snapshots — history exists on the target that only
// this key opens.
// Keeping it is harmless: a later target reuses it (WriteOffboxSecrets never re-mints over an
// existing file), and the escrow then matches by hash with nothing to redo.
//
// WHAT IT REFUSES (each leaves every file and setting untouched):
// - the hub-provisioned Felhom tier (Transport rclone-pinned) — that target is Felhom's to remove,
// not the page's; the hub would re-apply it anyway;
// - while a backup/restore/check holds the single-flight — a run reading the key file mid-delete;
// - while an abandonment countdown is running or awaiting the hub (R-241 / decision 74) — the
// countdown's state lives on the target record, and clearing it would strand the scheduled
// deletion of the set-aside history with nobody left to run or cancel it.
//
// Pinned by internal/backup/offbox_clear_r729_test.go.
// Refusal sentinels — the handler maps each to its own household sentence.
var (
ErrOffboxClearNoTarget = errors.New("offbox clear: no off-site target is configured")
ErrOffboxClearHubTier = errors.New("offbox clear: the Felhom-provided off-site tier cannot be removed from the box")
ErrOffboxClearBusy = errors.New("offbox clear: a backup operation is running")
ErrOffboxClearAbandonOn = errors.New("offbox clear: a set-aside history deletion is scheduled")
)
// OffboxClearResult says what the press did with the one secret it may keep.
type OffboxClearResult struct {
// KeptRepoPassword is true when the repository password stayed on disk (see the header for why).
KeptRepoPassword bool
}
// offboxRepoPasswordNeeded reports whether anything could still depend on the on-box repository
// password. Fail-safe direction: every input that MIGHT mean "something depends on it" keeps it.
func (m *Manager) offboxRepoPasswordNeeded(escrowState, lastSuccess string, snapshots int) bool {
return m.sealedPackageHeld() || escrowState == "escrowed" || lastSuccess != "" || snapshots > 0
}
// ClearOffboxTarget forgets the household's off-site target. See the file header.
func (m *Manager) ClearOffboxTarget() (OffboxClearResult, error) {
var res OffboxClearResult
t := m.settings.GetOffboxTarget()
if t == nil {
return res, ErrOffboxClearNoTarget
}
if t.Pinned() {
return res, ErrOffboxClearHubTier
}
if ab := m.AbandonStatus(); ab.Active || ab.PurgeRequested || ab.HubPending {
return res, ErrOffboxClearAbandonOn
}
// Hold the single-flight for the whole clear, so no run can start between the check and the delete.
if err := m.acquireRunning(); err != nil {
return res, ErrOffboxClearBusy
}
defer m.releaseRunning()
res.KeptRepoPassword = m.offboxRepoPasswordNeeded(t.EscrowState, t.LastSuccess, t.SnapshotCount)
// Files FIRST, settings LAST: a failure part-way leaves a target that is still listed (and no
// longer runnable — OffboxConfigured needs the key file), so the press can simply be repeated.
// The reverse order would leave secrets on disk with no target left to clear them through.
files := []string{m.offboxKeyPath(), m.offboxKnownHosts()}
if !res.KeptRepoPassword {
files = append(files, m.offboxPwPath())
}
for _, f := range files {
if err := os.Remove(f); err != nil && !os.IsNotExist(err) {
return res, fmt.Errorf("offbox clear: remove %s: %w", f, err)
}
}
if err := m.settings.SetOffboxTarget(nil); err != nil {
return res, fmt.Errorf("offbox clear: save settings: %w", err)
}
m.logger.Printf("[INFO] [offbox] off-site target %s@%s:%s CLEARED by the household (transport secrets deleted; repository password kept=%v; nothing on the target was touched)",
t.User, t.Host, t.RepoPath, res.KeptRepoPassword)
return res, nil
}
@@ -0,0 +1,160 @@
package backup
import (
"context"
"errors"
"path/filepath"
"testing"
"time"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
// R-729 / R-545 — the household can remove its own off-site target; the repository is never touched,
// and the repository password is kept whenever anything could depend on it. See offbox_clear.go.
func clearFixture(t *testing.T, hubHoldsPackage bool, tgt *settings.OffboxTarget) (*Manager, *settings.Settings, string) {
t.Helper()
m, sett, pwPath := mintGuardManager(t, false) // mint first, THEN set the package fact
if err := sett.SetOffboxTarget(tgt); err != nil {
t.Fatal(err)
}
if err := m.WriteOffboxSecrets("PRIVATE-KEY-MATERIAL", "nas.local ssh-ed25519 AAAAhostkey"); err != nil {
t.Fatal(err)
}
if err := sett.SetHubEscrowIdentityPresent(hubHoldsPackage); err != nil {
t.Fatal(err)
}
// Any remote command would go through the runner — none may run.
m.SetOffboxRunner(func(ctx context.Context, env []string, args ...string) ([]byte, error) {
t.Errorf("R-729: clearing a target ran restic %v — the repository must never be touched", args)
return nil, errors.New("forbidden")
})
m.SetOffboxSSH(func(ctx context.Context, host, user string, port int, keyPath, knownHosts, remoteCmd string) ([]byte, error) {
t.Errorf("R-729: clearing a target ran a remote command %q — the repository must never be touched", remoteCmd)
return nil, errors.New("forbidden")
})
return m, sett, filepath.Dir(pwPath)
}
func ownNAS() *settings.OffboxTarget {
return &settings.OffboxTarget{Enabled: false, Host: "nas.local", Port: 22, User: "felhom", RepoPath: "/srv/repo", EscrowState: "pending"}
}
// The headline: a never-used own-NAS target is forgotten and every secret it minted is gone.
func TestR729_Clear_ForgetsTargetAndDeletesSecrets(t *testing.T) {
m, sett, dir := clearFixture(t, false, ownNAS())
res, err := m.ClearOffboxTarget()
if err != nil {
t.Fatalf("clear: %v", err)
}
if sett.GetOffboxTarget() != nil {
t.Fatal("R-729: the target is still in settings after the clear")
}
for _, f := range []string{"ssh_key", "known_hosts", "repo_password"} {
if exists(filepath.Join(dir, f)) {
t.Errorf("R-545: %s is still on disk after clearing a target nothing depends on", f)
}
}
if res.KeptRepoPassword {
t.Error("KeptRepoPassword=true for a target with no escrow, no history and no hub package")
}
// Idempotent refusal: nothing left to clear.
if _, err := m.ClearOffboxTarget(); !errors.Is(err, ErrOffboxClearNoTarget) {
t.Errorf("second clear: want ErrOffboxClearNoTarget, got %v", err)
}
}
// The password is the one secret that may protect history — kept whenever anything depends on it.
func TestR729_Clear_KeepsRepoPasswordWhenSomethingDependsOnIt(t *testing.T) {
cases := []struct {
name string
pkg bool
mutate func(*settings.OffboxTarget)
}{
{"hub holds a sealed package (R-241)", true, func(*settings.OffboxTarget) {}},
{"escrow confirmed", false, func(o *settings.OffboxTarget) { o.EscrowState = "escrowed" }},
{"a run succeeded once", false, func(o *settings.OffboxTarget) { o.LastSuccess = "2026-10-01T02:00:00Z" }},
{"snapshots exist", false, func(o *settings.OffboxTarget) { o.SnapshotCount = 3 }},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
tgt := ownNAS()
c.mutate(tgt)
m, sett, dir := clearFixture(t, c.pkg, tgt)
res, err := m.ClearOffboxTarget()
if err != nil {
t.Fatalf("clear: %v", err)
}
if !exists(filepath.Join(dir, "repo_password")) {
t.Fatal("R-241: the repository password was deleted although something depends on it")
}
if !res.KeptRepoPassword {
t.Error("KeptRepoPassword=false although the key was kept")
}
if exists(filepath.Join(dir, "ssh_key")) || exists(filepath.Join(dir, "known_hosts")) {
t.Error("the transport secrets must still be deleted")
}
if sett.GetOffboxTarget() != nil {
t.Error("the target must still be forgotten")
}
})
}
}
// Refusals leave every file and the setting exactly as they were.
func TestR729_Clear_Refusals(t *testing.T) {
check := func(t *testing.T, m *Manager, sett *settings.Settings, dir string, want error) {
t.Helper()
if _, err := m.ClearOffboxTarget(); !errors.Is(err, want) {
t.Fatalf("want %v, got %v", want, err)
}
if sett.GetOffboxTarget() == nil {
t.Error("a refused clear removed the target")
}
for _, f := range []string{"ssh_key", "known_hosts", "repo_password"} {
if !exists(filepath.Join(dir, f)) {
t.Errorf("a refused clear deleted %s", f)
}
}
}
t.Run("hub tier", func(t *testing.T) {
tgt := ownNAS()
tgt.Transport = settings.TransportRclonePinned
m, sett, dir := clearFixture(t, false, tgt)
check(t, m, sett, dir, ErrOffboxClearHubTier)
})
t.Run("run in flight", func(t *testing.T) {
m, sett, dir := clearFixture(t, false, ownNAS())
if err := m.acquireRunning(); err != nil {
t.Fatal(err)
}
defer m.releaseRunning()
check(t, m, sett, dir, ErrOffboxClearBusy)
})
t.Run("abandonment countdown", func(t *testing.T) {
tgt := ownNAS()
tgt.AbandonStartedAt = time.Now().UTC().Format(time.RFC3339)
tgt.AbandonAt = time.Now().Add(7 * 24 * time.Hour).UTC().Format(time.RFC3339)
tgt.AbandonRepoPath = "/srv/repo.orphaned-1"
m, sett, dir := clearFixture(t, false, tgt)
check(t, m, sett, dir, ErrOffboxClearAbandonOn)
})
t.Run("hub-held deletion", func(t *testing.T) {
tgt := ownNAS()
tgt.AbandonHubDueAt = time.Now().Add(3 * 24 * time.Hour).UTC().Format(time.RFC3339)
m, sett, dir := clearFixture(t, false, tgt)
check(t, m, sett, dir, ErrOffboxClearAbandonOn)
})
}
// The single-flight is released after a clear — a later run is not wedged.
func TestR729_Clear_ReleasesSingleFlight(t *testing.T) {
m, _, _ := clearFixture(t, false, ownNAS())
if _, err := m.ClearOffboxTarget(); err != nil {
t.Fatal(err)
}
if m.IsRunning() {
t.Fatal("the clear left the single-flight held")
}
}
+8
View File
@@ -417,6 +417,9 @@
"backups_remote.megosztott_mappak_mentese": "Backing up shared folders…",
"backups_remote.mely_alkalmazasok_mentodnek_a_tavoli": "Which apps are backed up to the remote store?",
"backups_remote.mentes": "Save",
"backups_remote.tavoli_mentesi_cel_torlese": "Remove the remote backup destination",
"backups_remote.cel_torlese_magyarazat": "The box forgets this remote backup destination and deletes the SSH key that belongs to it. It does not touch the backups on the destination: they stay where they are. No remote backup runs after this until you set up a new destination.",
"backups_remote.cel_torlese_megerosites": "Yes, remove the destination",
"backups_remote.mentes_2": "Backup:",
"backups_remote.mentes_folyamatban": "Backup in progress",
"backups_remote.napi_automatikus_tavoli_mentes_engedelye": "Daily automatic remote backup allowed",
@@ -1419,6 +1422,11 @@
"flash.offbox.mgr_unreachable": "Backups cannot be managed right now. Try again in a few minutes; if it still does not work, contact Felhom support.",
"flash.offbox.needs_confirmation": "The restore needs your confirmation.",
"flash.offbox.not_orphaned": "There is no need to start a new remote backup: your current remote backup opens normally and keeps working. If you still see a problem with it, contact Felhom support.",
"flash.offbox.clear_needs_confirmation": "Removing the destination needs a confirmation.",
"flash.offbox.clear_hub_tier": "This remote storage is provided by Felhom, so it cannot be removed here. If you no longer want it, contact Felhom support.",
"flash.offbox.clear_busy": "A backup or a restore is running now. Wait until it finishes, then try again.",
"flash.offbox.clear_abandon_pending": "The deletion of the old, set-aside remote backups is in progress. While it runs, the destination cannot be removed. Try again later.",
"flash.offbox.target_cleared": "The remote backup destination is removed. The backups on the destination were not touched.",
"flash.offbox.path_absolute": "The storage path must be absolute (it has to start with /).",
"flash.offbox.recover_started": "The recovery has started — the status updates here.",
"flash.offbox.repo_orphaned": "The remote store is orphaned — start a new remote backup first, the way the card shows.",
+8
View File
@@ -413,6 +413,9 @@
"backups_remote.megosztott_mappak_mentese": "Megosztott mappák mentése…",
"backups_remote.mely_alkalmazasok_mentodnek_a_tavoli": "Mely alkalmazások mentődnek a távoli tárolóra?",
"backups_remote.mentes": "Mentés",
"backups_remote.tavoli_mentesi_cel_torlese": "Távoli mentési cél törlése",
"backups_remote.cel_torlese_magyarazat": "A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.",
"backups_remote.cel_torlese_megerosites": "Igen, töröld a célt",
"backups_remote.mentes_2": "Mentés:",
"backups_remote.mentes_folyamatban": "Mentés folyamatban",
"backups_remote.napi_automatikus_tavoli_mentes_engedelye": "Napi automatikus távoli mentés engedélyezve",
@@ -1410,6 +1413,11 @@
"flash.offbox.mgr_unreachable": "A mentések kezelése most nem érhető el. Próbáld újra néhány perc múlva; ha akkor sem megy, keresd a Felhom ügyfélszolgálatát.",
"flash.offbox.needs_confirmation": "A visszaállításhoz megerősítés szükséges.",
"flash.offbox.not_orphaned": "Új távoli mentést nem kell indítani: a mostani távoli mentésed rendben megnyitható, és tovább működik. Ha mégis gondot látsz vele, keresd a Felhom ügyfélszolgálatát.",
"flash.offbox.clear_needs_confirmation": "A cél törléséhez megerősítés szükséges.",
"flash.offbox.clear_hub_tier": "Ezt a távoli tárhelyet a Felhom adja, ezért itt nem törölhető. Ha nem kéred tovább, keresd a Felhom ügyfélszolgálatát.",
"flash.offbox.clear_busy": "Most mentés vagy visszaállítás fut. Várd meg, amíg befejeződik, aztán próbáld újra.",
"flash.offbox.clear_abandon_pending": "A régi, félretett távoli mentések törlése folyamatban van. Amíg ez tart, a cél nem törölhető. Próbáld újra később.",
"flash.offbox.target_cleared": "A távoli mentési cél törölve. A célgépen lévő mentésekhez nem nyúltunk.",
"flash.offbox.path_absolute": "A tárhely útvonalának abszolútnak kell lennie (/-rel kezdődjön).",
"flash.offbox.recover_started": "A helyreállítás elindult — az állapot itt frissül.",
"flash.offbox.repo_orphaned": "A távoli tároló elárvult — előbb indíts új távoli mentést a kártyán látható módon.",
@@ -331,6 +331,49 @@ func (s *Server) offboxResetHandler(w http.ResponseWriter, r *http.Request) {
offboxRedirect(w, r, "flash.offbox.restart_started", false)
}
// offboxClearHandler is the household's „Távoli mentési cél törlése" press (R-729 / R-545): forget the
// target and delete its transport secrets, never the repository. Reveal-then-confirm like the reset
// (confirm=1). The refusals and what is kept are backup.ClearOffboxTarget's; this maps them to words.
func (s *Server) offboxClearHandler(w http.ResponseWriter, r *http.Request) {
if s.backupMgr == nil {
offboxRedirect(w, r, "flash.offbox.mgr_unavailable", true)
return
}
_ = r.ParseForm()
if r.FormValue("confirm") != "1" {
offboxRedirect(w, r, "flash.offbox.clear_needs_confirmation", true)
return
}
res, err := s.backupMgr.ClearOffboxTarget()
switch {
case errors.Is(err, backup.ErrOffboxClearNoTarget):
offboxRedirect(w, r, "flash.offbox.target_not_set", true)
return
case errors.Is(err, backup.ErrOffboxClearHubTier):
offboxRedirect(w, r, "flash.offbox.clear_hub_tier", true)
return
case errors.Is(err, backup.ErrOffboxClearBusy):
offboxRedirect(w, r, "flash.offbox.clear_busy", true)
return
case errors.Is(err, backup.ErrOffboxClearAbandonOn):
offboxRedirect(w, r, "flash.offbox.clear_abandon_pending", true)
return
case err != nil:
s.logger.Printf("[ERROR] [web] off-site target clear failed: %v", err)
offboxRedirect(w, r, "flash.offbox.save_failed", true)
return
}
if !res.KeptRepoPassword {
// The key is gone, so a copy still staged on the agent for an unfinished ceremony is a stale
// secret with nothing behind it. Best-effort, like the confirm path's wipe.
if werr := s.wipeStagedEscrow(r.Context()); werr != nil {
s.logger.Printf("[WARN] [web] off-site target cleared, but the agent-staged escrow copy was not wiped: %v", werr)
}
}
s.reportTriggerNow()
offboxRedirect(w, r, "flash.offbox.target_cleared", false)
}
// offboxStatusHandler (Part C) is the poll source for the remote-backup run status — the page polls it
// after "Távoli mentés most" and flips to the terminal state without a manual reload. Session-auth'd.
func (s *Server) offboxStatusHandler(w http.ResponseWriter, r *http.Request) {
@@ -0,0 +1,119 @@
package web
import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
)
// R-729 / R-545 — the „Távoli mentési cél törlése" press: the route is wired, needs confirm=1, and
// clears the household's own target; the page offers it for an own target and NOT for the Felhom tier.
func clearServer(t *testing.T, tgt *settings.OffboxTarget) (*Server, string) {
t.Helper()
s := noteServer(t)
s.cfg.Paths.DataDir = t.TempDir()
bm := backup.NewManager(s.cfg, s.settings, s.logger)
if err := s.settings.SetOffboxTarget(tgt); err != nil {
t.Fatal(err)
}
if err := bm.WriteOffboxSecrets("KEYMATERIAL", "nas.local ssh-ed25519 HOSTKEY"); err != nil {
t.Fatal(err)
}
s.backupMgr = bm
s.wipeStagedEscrowFn = func(context.Context) error { return nil }
return s, filepath.Join(s.cfg.Paths.DataDir, "offbox")
}
func postClear(t *testing.T, s *Server, form string) (flash, flashErr string) {
t.Helper()
req := httptest.NewRequest(http.MethodPost, "/backup/offbox/clear", strings.NewReader(form))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
w := httptest.NewRecorder()
s.offboxClearHandler(w, req)
if w.Code != http.StatusFound {
t.Fatalf("want redirect, got %d", w.Code)
}
u, _ := url.Parse(w.Header().Get("Location"))
return u.Query().Get("flash"), u.Query().Get("flash_error")
}
func TestR729_ClearHandler_NeedsConfirmAndThenClears(t *testing.T) {
s, dir := clearServer(t, &settings.OffboxTarget{Enabled: false, Host: "nas.local", Port: 22, User: "felhom", RepoPath: "/srv/repo"})
// Without confirm=1 nothing happens.
if _, fe := postClear(t, s, ""); fe != "flash.offbox.clear_needs_confirmation" {
t.Fatalf("no-confirm: flash_error=%q", fe)
}
if s.settings.GetOffboxTarget() == nil {
t.Fatal("an unconfirmed press removed the target")
}
f, fe := postClear(t, s, "confirm=1")
if fe != "" || f != "flash.offbox.target_cleared" {
t.Fatalf("confirmed clear: flash=%q flash_error=%q", f, fe)
}
if s.settings.GetOffboxTarget() != nil {
t.Fatal("R-729: the target survived a confirmed clear")
}
if _, err := os.Stat(filepath.Join(dir, "ssh_key")); !os.IsNotExist(err) {
t.Fatalf("R-545: the SSH key survived a confirmed clear (stat err=%v)", err)
}
// Both languages say the backups on the destination were not touched.
if hu := s.msgLang("hu", f); !strings.Contains(hu, "nem ny") {
t.Errorf("hu success text does not say the destination was untouched: %q", hu)
}
if en := s.msgLang("en", f); !strings.Contains(en, "not touched") {
t.Errorf("en success text does not say the destination was untouched: %q", en)
}
}
func TestR729_ClearHandler_RefusesHubTier(t *testing.T) {
s, dir := clearServer(t, &settings.OffboxTarget{Enabled: true, Host: "box.example", Port: 23, User: "u1", RepoPath: "/home/repo", Transport: settings.TransportRclonePinned})
if _, fe := postClear(t, s, "confirm=1"); fe != "flash.offbox.clear_hub_tier" {
t.Fatalf("hub tier: flash_error=%q", fe)
}
if s.settings.GetOffboxTarget() == nil {
t.Fatal("the Felhom tier was removed from the box")
}
if _, err := os.Stat(filepath.Join(dir, "ssh_key")); err != nil {
t.Fatalf("the Felhom tier's key was deleted: %v", err)
}
}
// Render test per branch of the template gate (seam-built-but-never-wired lesson).
func TestR729_RemotePage_OffersClearOnlyForOwnTarget(t *testing.T) {
d := splitTestData()
html := renderBackupPage(t, "backups_remote", d)
if !strings.Contains(html, `action="/backup/offbox/clear"`) {
t.Fatal("R-729: an own target renders no clear press")
}
d["Offbox"] = &settings.OffboxTarget{Enabled: true, Host: "box.example", LastStatus: "ok", EscrowState: "escrowed", Transport: settings.TransportRclonePinned}
if strings.Contains(renderBackupPage(t, "backups_remote", d), `action="/backup/offbox/clear"`) {
t.Fatal("R-729: the Felhom tier must not be offered for removal")
}
d["Offbox"] = nil
d["OffboxConfigured"] = false
if strings.Contains(renderBackupPage(t, "backups_remote", d), `action="/backup/offbox/clear"`) {
t.Fatal("R-729: no target, yet a clear press renders")
}
}
// The route reaches the handler (a case only in a comment is the decoy this guards against).
func TestR729_ClearRouteIsWired(t *testing.T) {
src, err := os.ReadFile("server.go")
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(src), `path == "/backup/offbox/clear" && r.Method == http.MethodPost:`+" // R-729 / R-545\n\t\ts.offboxClearHandler(w, r)") {
t.Fatal("R-729: /backup/offbox/clear is not routed to offboxClearHandler")
}
}
+2
View File
@@ -753,6 +753,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.offboxRunHandler(w, r)
case path == "/backup/offbox/reset" && r.Method == http.MethodPost:
s.offboxResetHandler(w, r)
case path == "/backup/offbox/clear" && r.Method == http.MethodPost: // R-729 / R-545
s.offboxClearHandler(w, r)
case path == "/backup/offbox/status" && r.Method == http.MethodGet:
s.offboxStatusHandler(w, r)
case path == "/backup/offbox/restore" && r.Method == http.MethodPost:
@@ -270,6 +270,20 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">{{T "backups_remote.mentes"}}</button>
</form>
</details>
{{/* R-729 / R-545: the household's own target can be removed. Never the repository on it; the
Felhom-provided tier (Pinned) is not offered. Reveal-then-confirm, like the orphan reset. */}}
{{if .Offbox}}{{if not .Offbox.Pinned}}
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">{{T "backups_remote.tavoli_mentesi_cel_torlese"}}</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">{{T "backups_remote.cel_torlese_magyarazat"}}</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">{{.CSRFField}}
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">{{T "backups_remote.cel_torlese_megerosites"}}</button>
</form>
</div>
</div>
{{end}}{{end}}
</div>
{{end}}
@@ -276,6 +276,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -262,6 +262,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -265,6 +265,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -363,6 +363,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -277,6 +277,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -213,6 +213,8 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
</div>
@@ -214,6 +214,8 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
</div>
@@ -278,6 +278,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -268,6 +268,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -268,6 +268,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -282,6 +282,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -266,6 +266,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>
@@ -266,6 +266,19 @@
<button type="submit" class="btn btn-sm btn-primary" style="margin-top:.5rem">Mentés</button>
</form>
</details>
<div style="margin-top:.75rem" id="offbox-clear">
<button type="button" class="btn btn-xs btn-outline" id="offbox-clear-reveal" onclick="var c=document.getElementById('offbox-clear-confirm');c.style.display='block';this.style.display='none'">Távoli mentési cél törlése</button>
<div id="offbox-clear-confirm" style="display:none;margin-top:.6rem">
<p class="form-hint" style="margin:0 0 .5rem">A gép elfelejti ezt a távoli mentési célt, és törli a hozzá tartozó SSH kulcsot. A célgépen lévő mentésekhez nem nyúl: azok ott maradnak, ahol vannak. Ezután nem fut távoli mentés, amíg új célt nem állítasz be.</p>
<form method="POST" action="/backup/offbox/clear" style="display:inline">
<input type="hidden" name="confirm" value="1">
<button type="submit" class="btn btn-sm btn-primary">Igen, töröld a célt</button>
</form>
</div>
</div>
</div>