Files
曾志威 9da95beb42
continuous-integration/drone Build is failing
feat: refine management UI, connection diagnostics and local startup
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.
2026-10-13 19:43:23 +08:00

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")
}
}