package api import ( "errors" "linkport/internal/store" "net/http" "strconv" "strings" ) func (s *Server) configurationStopped(w http.ResponseWriter) bool { ps, e := s.store.List() if e != nil { fail(w, 500, "cannot load gateways") return false } for _, p := range ps { if s.manager.Status(p.ID).Running { fail(w, 409, "stop all gateways before importing or restoring configuration") return false } } return true } func (s *Server) configuration(w http.ResponseWriter, r *http.Request, path string) { action := strings.TrimPrefix(path, "configuration/") w.Header().Set("Cache-Control", "no-store") switch action { case "startup": s.startupSettings(w, r) case "scopes": s.scopeCatalog(w, r) case "diff": if r.Method != "GET" { fail(w, 405, "method not allowed") return } q := r.URL.Query() revision, err := strconv.ParseInt(q.Get("revision"), 10, 64) if err != nil || revision < 1 { fail(w, 400, "invalid revision") return } changes, err := s.store.ConfigurationDiff(q.Get("kind"), q.Get("id"), revision) if err != nil { fail(w, 400, "cannot compare configuration revision") return } respond(w, 200, map[string]any{"revision": revision, "changes": changes}) case "backup": if r.Method != "POST" { fail(w, 405, "method not allowed") return } var body struct { Passphrase string `json:"passphrase"` } if !decode(w, r, &body) { return } b, e := s.store.Backup(body.Passphrase) if e != nil { fail(w, 400, "backup failed; passphrase must contain 12-1024 bytes") return } s.store.Event("", source(r), "configuration_backup", "") w.Header().Set("Content-Disposition", `attachment; filename="linkport-backup.json"`) respond(w, 200, b) case "import": if r.Method != "POST" { fail(w, 405, "method not allowed") return } var body struct { ExpectedRevision string `json:"expectedRevision"` Bundle *store.Bundle `json:"bundle"` Backup *store.EncryptedBackup `json:"backup"` Passphrase string `json:"passphrase"` } if !decodeLimit(w, r, &body, 24<<20) { return } if (body.Bundle == nil) == (body.Backup == nil) { fail(w, 400, "provide exactly one bundle or encrypted backup") return } var b store.Bundle if body.Backup != nil { var e error b, e = store.OpenBackup(*body.Backup, body.Passphrase) if e != nil { fail(w, 400, "incorrect passphrase or invalid backup") return } } else { b = *body.Bundle } if !s.configurationStopped(w) { return } if e := s.store.RestoreBundleChecked(b, body.ExpectedRevision); e != nil { if errors.Is(e, store.ErrConfigurationChanged) { fail(w, 409, e.Error()) return } fail(w, 400, "configuration import failed: "+e.Error()) return } s.store.Event("", source(r), "configuration_imported", "") respond(w, 200, map[string]bool{"ok": true}) case "import/preview": if r.Method != "POST" { fail(w, 405, "method not allowed") return } var body struct { Bundle *store.Bundle `json:"bundle"` Backup *store.EncryptedBackup `json:"backup"` Passphrase string `json:"passphrase"` } if !decodeLimit(w, r, &body, 24<<20) { return } if (body.Bundle == nil) == (body.Backup == nil) { fail(w, 400, "provide exactly one bundle or encrypted backup") return } var b store.Bundle var err error if body.Bundle != nil { b = *body.Bundle } else { b, err = store.OpenBackup(*body.Backup, body.Passphrase) } if err != nil { fail(w, 400, "invalid backup") return } preview, err := s.store.PreviewBundle(b) if err != nil { fail(w, 400, err.Error()) return } respond(w, 200, preview) case "history": if r.Method != "GET" { fail(w, 405, "method not allowed") return } h, e := s.store.History(r.URL.Query().Get("kind"), r.URL.Query().Get("id")) if e != nil { fail(w, 400, "cannot load configuration history") return } respond(w, 200, h) case "restore": if r.Method != "POST" { fail(w, 405, "method not allowed") return } var body struct { Kind string `json:"kind"` ID string `json:"id"` Revision int64 `json:"revision"` } if !decode(w, r, &body) { return } if !s.configurationStopped(w) { return } if e := s.store.RestoreRevision(body.Kind, body.ID, body.Revision); e != nil { fail(w, 400, "configuration revision cannot be restored") return } s.store.Event(body.ID, source(r), "configuration_restored", body.Kind) respond(w, 200, map[string]bool{"ok": true}) default: fail(w, 404, "not found") } }