From d6b83a0c1bec66f3d628519adaf610b54bdc0abe Mon Sep 17 00:00:00 2001 From: sagit Date: Wed, 11 Feb 2026 02:48:57 +0000 Subject: [PATCH 1/3] feat(panel-sharing): add edit support for peer shares Allow editing port range, traffic limit, allowed domains, allowed API IPs and expiry time on existing shares via a new update endpoint and edit modal in the frontend. --- .../internal/http/handler/federation.go | 85 ++++++++++++ go-backend/internal/http/handler/handler.go | 1 + vite-frontend/src/api/index.ts | 10 ++ vite-frontend/src/pages/panel-sharing.tsx | 127 ++++++++++++++++++ 4 files changed, 223 insertions(+) diff --git a/go-backend/internal/http/handler/federation.go b/go-backend/internal/http/handler/federation.go index e3bea0d..4f6c26f 100644 --- a/go-backend/internal/http/handler/federation.go +++ b/go-backend/internal/http/handler/federation.go @@ -40,6 +40,17 @@ type resetPeerShareFlowRequest struct { ID int64 `json:"id"` } +type updatePeerShareRequest struct { + ID int64 `json:"id"` + Name string `json:"name"` + MaxBandwidth int64 `json:"maxBandwidth"` + ExpiryTime int64 `json:"expiryTime"` + PortRangeStart int `json:"portRangeStart"` + PortRangeEnd int `json:"portRangeEnd"` + AllowedDomains string `json:"allowedDomains"` + AllowedIPs string `json:"allowedIps"` +} + type nodeImportRequest struct { RemoteURL string `json:"remoteUrl"` Token string `json:"token"` @@ -325,6 +336,80 @@ func (h *Handler) federationShareResetFlow(w http.ResponseWriter, r *http.Reques response.WriteJSON(w, response.OKEmpty()) } +func (h *Handler) federationShareUpdate(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + response.WriteJSON(w, response.ErrDefault("Invalid method")) + return + } + + var req updatePeerShareRequest + if err := decodeJSON(r.Body, &req); err != nil { + response.WriteJSON(w, response.ErrDefault("Invalid JSON")) + return + } + if req.ID <= 0 { + response.WriteJSON(w, response.ErrDefault("Share ID is required")) + return + } + + share, err := h.repo.GetPeerShare(req.ID) + if err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + if share == nil { + response.WriteJSON(w, response.ErrDefault("Share not found")) + return + } + + if req.Name == "" { + response.WriteJSON(w, response.ErrDefault("Name is required")) + return + } + + if req.MaxBandwidth < 0 { + response.WriteJSON(w, response.ErrDefault("Max bandwidth cannot be negative")) + return + } + + if req.ExpiryTime < 0 { + response.WriteJSON(w, response.ErrDefault("Expiry time cannot be negative")) + return + } + + if req.PortRangeStart < 0 || req.PortRangeStart > 65535 || req.PortRangeEnd < 0 || req.PortRangeEnd > 65535 { + response.WriteJSON(w, response.ErrDefault("Invalid port range")) + return + } + + if req.PortRangeStart > req.PortRangeEnd { + response.WriteJSON(w, response.ErrDefault("Port range start cannot be greater than end")) + return + } + + allowedIPs, err := normalizePeerShareAllowedIPs(req.AllowedIPs) + if err != nil { + response.WriteJSON(w, response.ErrDefault(err.Error())) + return + } + + share.Name = req.Name + share.MaxBandwidth = req.MaxBandwidth + share.ExpiryTime = req.ExpiryTime + share.PortRangeStart = req.PortRangeStart + share.PortRangeEnd = req.PortRangeEnd + share.AllowedDomains = req.AllowedDomains + share.AllowedIPs = allowedIPs + share.UpdatedTime = time.Now().UnixMilli() + + if err := h.repo.UpdatePeerShare(share); err != nil { + response.WriteJSON(w, response.Err(-2, err.Error())) + return + } + + response.WriteJSON(w, response.OKEmpty()) +} + func (h *Handler) federationRemoteUsageList(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { response.WriteJSON(w, response.ErrDefault("Invalid method")) diff --git a/go-backend/internal/http/handler/handler.go b/go-backend/internal/http/handler/handler.go index 4bfec49..a0ce67c 100644 --- a/go-backend/internal/http/handler/handler.go +++ b/go-backend/internal/http/handler/handler.go @@ -155,6 +155,7 @@ func (h *Handler) Register(mux *http.ServeMux) { mux.HandleFunc("/api/v1/open_api/sub_store", h.openAPISubStore) mux.HandleFunc("/api/v1/federation/share/list", h.federationShareList) mux.HandleFunc("/api/v1/federation/share/create", h.federationShareCreate) + mux.HandleFunc("/api/v1/federation/share/update", h.federationShareUpdate) mux.HandleFunc("/api/v1/federation/share/delete", h.federationShareDelete) mux.HandleFunc("/api/v1/federation/share/reset-flow", h.federationShareResetFlow) mux.HandleFunc("/api/v1/federation/share/remote-usage/list", h.federationRemoteUsageList) diff --git a/vite-frontend/src/api/index.ts b/vite-frontend/src/api/index.ts index f0b56d2..bd8c71e 100644 --- a/vite-frontend/src/api/index.ts +++ b/vite-frontend/src/api/index.ts @@ -200,6 +200,16 @@ export const createPeerShare = (data: { allowedDomains?: string; allowedIps?: string; }) => Network.post("/federation/share/create", data); +export const updatePeerShare = (data: { + id: number; + name: string; + maxBandwidth: number; + expiryTime: number; + portRangeStart: number; + portRangeEnd: number; + allowedDomains: string; + allowedIps: string; +}) => Network.post("/federation/share/update", data); export const deletePeerShare = (id: number) => Network.post("/federation/share/delete", { id }); export const resetPeerShareFlow = (id: number) => diff --git a/vite-frontend/src/pages/panel-sharing.tsx b/vite-frontend/src/pages/panel-sharing.tsx index fdd881c..2b5205a 100644 --- a/vite-frontend/src/pages/panel-sharing.tsx +++ b/vite-frontend/src/pages/panel-sharing.tsx @@ -20,6 +20,7 @@ import { resetPeerShareFlow, getPeerRemoteUsageList, importRemoteNode, + updatePeerShare, } from "@/api"; interface Node { @@ -91,6 +92,7 @@ export default function PanelSharingPage() { // Modals const [createShareOpen, setCreateShareOpen] = useState(false); + const [editShareOpen, setEditShareOpen] = useState(false); const [importNodeOpen, setImportNodeOpen] = useState(false); // Forms @@ -110,6 +112,17 @@ export default function PanelSharingPage() { token: "", }); + const [editForm, setEditForm] = useState({ + id: 0, + name: "", + maxBandwidth: 0, + expiryTime: 0, + portRangeStart: 10000, + portRangeEnd: 20000, + allowedDomains: "", + allowedIps: "", + }); + const loadShares = useCallback(async () => { setLoading(true); try { @@ -239,6 +252,52 @@ export default function PanelSharingPage() { } }; + const openEditShare = (share: PeerShare) => { + setEditForm({ + id: share.id, + name: share.name, + maxBandwidth: share.maxBandwidth > 0 ? Math.round(share.maxBandwidth / (1024 * 1024 * 1024)) : 0, + expiryTime: share.expiryTime, + portRangeStart: share.portRangeStart, + portRangeEnd: share.portRangeEnd, + allowedDomains: share.allowedDomains || "", + allowedIps: share.allowedIps || "", + }); + setEditShareOpen(true); + }; + + const handleEditShare = async () => { + if (!editForm.name) { + toast.error("名称不能为空"); + return; + } + if (editForm.maxBandwidth < 0) { + toast.error("流量上限不能为负数"); + return; + } + try { + const res = await updatePeerShare({ + id: editForm.id, + name: editForm.name, + maxBandwidth: Math.max(0, editForm.maxBandwidth) * 1024 * 1024 * 1024, + expiryTime: editForm.expiryTime, + portRangeStart: editForm.portRangeStart, + portRangeEnd: editForm.portRangeEnd, + allowedDomains: editForm.allowedDomains, + allowedIps: editForm.allowedIps, + }); + if (res.code === 0) { + toast.success("编辑成功"); + setEditShareOpen(false); + loadShares(); + } else { + toast.error(res.msg || "编辑失败"); + } + } catch { + toast.error("网络错误"); + } + }; + const handleImportNode = async () => { if (!importForm.remoteUrl || !importForm.token) { toast.error("请填写完整信息"); @@ -326,6 +385,13 @@ export default function PanelSharingPage() {

{share.name}

+ + + + + + {/* Import Node Modal */} setImportNodeOpen(false)}> From 87605ce8f8aca2bbdccd5604ba36baa7fa1c1ad9 Mon Sep 17 00:00:00 2001 From: sagit Date: Wed, 11 Feb 2026 03:07:42 +0000 Subject: [PATCH 2/3] fix(federation): sync remote node status on list and detect deleted provider shares Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- go-backend/internal/http/client/federation.go | 8 +++ .../internal/http/handler/federation.go | 69 +++++++++++++++++++ go-backend/internal/http/handler/handler.go | 3 + 3 files changed, 80 insertions(+) diff --git a/go-backend/internal/http/client/federation.go b/go-backend/internal/http/client/federation.go index fdc04ee..128055a 100644 --- a/go-backend/internal/http/client/federation.go +++ b/go-backend/internal/http/client/federation.go @@ -85,6 +85,14 @@ func NewFederationClient() *FederationClient { } } +func NewFederationClientWithTimeout(timeout time.Duration) *FederationClient { + return &FederationClient{ + client: &http.Client{ + Timeout: timeout, + }, + } +} + func (c *FederationClient) Connect(url, token, localDomain string) (*RemoteNodeInfo, error) { url = strings.TrimSuffix(url, "/") req, err := http.NewRequest("POST", url+"/api/v1/federation/connect", nil) diff --git a/go-backend/internal/http/handler/federation.go b/go-backend/internal/http/handler/federation.go index 4f6c26f..625357f 100644 --- a/go-backend/internal/http/handler/federation.go +++ b/go-backend/internal/http/handler/federation.go @@ -8,6 +8,7 @@ import ( "net/http" "sort" "strings" + "sync" "time" "go-backend/internal/http/client" @@ -1402,6 +1403,74 @@ func isPeerIPAllowed(clientIP net.IP, whitelist string) bool { return false } +func (h *Handler) syncRemoteNodeStatuses(items []map[string]interface{}) { + type remoteEntry struct { + index int + remoteURL string + remoteToken string + } + + var remotes []remoteEntry + for i, item := range items { + isRemote, _ := item["isRemote"].(int) + if isRemote != 1 { + continue + } + url, _ := item["remoteUrl"].(string) + token, _ := item["remoteToken"].(string) + url = strings.TrimSpace(url) + token = strings.TrimSpace(token) + if url == "" || token == "" { + continue + } + remotes = append(remotes, remoteEntry{index: i, remoteURL: url, remoteToken: token}) + } + if len(remotes) == 0 { + return + } + + localDomain := h.federationLocalDomain() + fc := client.NewFederationClientWithTimeout(5 * time.Second) + + type syncResult struct { + index int + status int + syncError string + } + + results := make([]syncResult, len(remotes)) + var wg sync.WaitGroup + for i, entry := range remotes { + wg.Add(1) + go func(idx int, e remoteEntry) { + defer wg.Done() + info, err := fc.Connect(e.remoteURL, e.remoteToken, localDomain) + if err != nil { + errMsg := err.Error() + if strings.Contains(errMsg, "401") || strings.Contains(errMsg, "Invalid token") || strings.Contains(errMsg, "Unauthorized") { + results[idx] = syncResult{index: e.index, status: 0, syncError: "provider_share_deleted"} + } else if strings.Contains(errMsg, "403") || strings.Contains(errMsg, "Share is disabled") { + results[idx] = syncResult{index: e.index, status: 0, syncError: "provider_share_disabled"} + } else if strings.Contains(errMsg, "Share expired") { + results[idx] = syncResult{index: e.index, status: 0, syncError: "provider_share_expired"} + } else { + results[idx] = syncResult{index: e.index, status: 0, syncError: errMsg} + } + } else { + results[idx] = syncResult{index: e.index, status: info.Status, syncError: ""} + } + }(i, entry) + } + wg.Wait() + + for _, r := range results { + items[r.index]["status"] = r.status + if r.syncError != "" { + items[r.index]["syncError"] = r.syncError + } + } +} + func (h *Handler) cleanupPeerShareRuntimes(shareID int64) { if h == nil || h.repo == nil || shareID <= 0 { return diff --git a/go-backend/internal/http/handler/handler.go b/go-backend/internal/http/handler/handler.go index a0ce67c..9319956 100644 --- a/go-backend/internal/http/handler/handler.go +++ b/go-backend/internal/http/handler/handler.go @@ -321,6 +321,9 @@ func (h *Handler) nodeList(w http.ResponseWriter, r *http.Request) { response.WriteJSON(w, response.Err(-2, err.Error())) return } + + h.syncRemoteNodeStatuses(items) + response.WriteJSON(w, response.OK(items)) } From 896f2bc5f8a3a9a46faff03d32fd05203ce34351 Mon Sep 17 00:00:00 2001 From: sagit Date: Wed, 11 Feb 2026 03:07:59 +0000 Subject: [PATCH 3/3] fix(panel-sharing): display provider share deletion warnings for remote nodes Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- vite-frontend/src/pages/node.tsx | 15 ++++++++++++++- vite-frontend/src/pages/panel-sharing.tsx | 12 ++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/vite-frontend/src/pages/node.tsx b/vite-frontend/src/pages/node.tsx index 178c0b5..225e8cf 100644 --- a/vite-frontend/src/pages/node.tsx +++ b/vite-frontend/src/pages/node.tsx @@ -65,6 +65,7 @@ interface Node { status: number; isRemote?: number; remoteUrl?: string; + syncError?: string; connectionStatus: "online" | "offline"; systemInfo?: { cpuUsage: number; @@ -219,7 +220,8 @@ export default function NodePage() { const nodesData: Node[] = (res.data || []).map((node: any) => ({ ...node, inx: node.inx ?? 0, - connectionStatus: node.status === 1 ? "online" : "offline", + connectionStatus: node.syncError ? "offline" : node.status === 1 ? "online" : "offline", + syncError: node.syncError || undefined, systemInfo: null, copyLoading: false, })); @@ -1181,6 +1183,17 @@ export default function NodePage() { + {isRemoteNode && node.syncError && ( +
+ {node.syncError === "provider_share_deleted" + ? "提供方已删除该分享" + : node.syncError === "provider_share_disabled" + ? "提供方已禁用该分享" + : node.syncError === "provider_share_expired" + ? "提供方分享已过期" + : `远程同步失败: ${node.syncError}`} +
+ )} {/* 基础信息 */}
diff --git a/vite-frontend/src/pages/panel-sharing.tsx b/vite-frontend/src/pages/panel-sharing.tsx index 2b5205a..df05e73 100644 --- a/vite-frontend/src/pages/panel-sharing.tsx +++ b/vite-frontend/src/pages/panel-sharing.tsx @@ -78,6 +78,7 @@ interface RemoteUsageNode { usedPorts: number[]; bindings: RemoteUsageBinding[]; activeBindingNum: number; + syncError?: string; } export default function PanelSharingPage() { @@ -456,6 +457,17 @@ export default function PanelSharingPage() { 绑定 {node.activeBindingNum || 0} + {node.syncError && ( +
+ {node.syncError === "provider_share_deleted" + ? "提供方已删除该分享" + : node.syncError === "provider_share_disabled" + ? "提供方已禁用该分享" + : node.syncError === "provider_share_expired" + ? "提供方分享已过期" + : `远程同步失败: ${node.syncError}`} +
+ )} {node.remoteUrl &&

远程地址: {node.remoteUrl}

}

共享ID: {node.shareId || "-"}

端口范围: {node.portRangeStart > 0 && node.portRangeEnd > 0 ? `${node.portRangeStart} - ${node.portRangeEnd}` : "-"}