continuous-integration/drone Build is failing
Migrate the workspace to React and Ant Design, improve account forms and responsive layouts, support outbound diagnostics and SSH fingerprint confirmation, and persist the management startup port. Preserve database migrations, remove S3 settings storage, and improve gateway audit handling.
182 lines
4.6 KiB
Go
182 lines
4.6 KiB
Go
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")
|
|
}
|
|
}
|