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.
This commit is contained in:
sagit
2026-02-11 02:48:57 +00:00
parent f786d60219
commit d6b83a0c1b
4 changed files with 223 additions and 0 deletions
@@ -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"))
@@ -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)
+10
View File
@@ -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) =>
+127
View File
@@ -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() {
<CardHeader className="flex justify-between">
<h3 className="font-bold">{share.name}</h3>
<div className="flex gap-2">
<Button
size="sm"
variant="flat"
onPress={() => openEditShare(share)}
>
编辑
</Button>
<Button
size="sm"
variant="flat"
@@ -491,6 +557,67 @@ export default function PanelSharingPage() {
</ModalContent>
</Modal>
{/* Edit Share Modal */}
<Modal isOpen={editShareOpen} onClose={() => setEditShareOpen(false)}>
<ModalContent>
<ModalHeader>编辑分享</ModalHeader>
<ModalBody>
<Input
label="名称"
placeholder="备注名称"
value={editForm.name}
onChange={(e) => setEditForm({ ...editForm, name: e.target.value })}
/>
<div className="flex gap-4">
<Input
label="起始端口"
type="number"
value={editForm.portRangeStart.toString()}
onChange={(e) => setEditForm({ ...editForm, portRangeStart: parseInt(e.target.value) || 0 })}
/>
<Input
label="结束端口"
type="number"
value={editForm.portRangeEnd.toString()}
onChange={(e) => setEditForm({ ...editForm, portRangeEnd: parseInt(e.target.value) || 0 })}
/>
</div>
<Input
label="流量上限 (GB)"
type="number"
description="0 表示不限流量"
value={editForm.maxBandwidth.toString()}
onChange={(e) => setEditForm({ ...editForm, maxBandwidth: parseInt(e.target.value, 10) || 0 })}
/>
<Input
label="过期时间"
type="datetime-local"
description="留空或清除表示永久有效"
value={editForm.expiryTime > 0 ? new Date(editForm.expiryTime).toISOString().slice(0, 16) : ""}
onChange={(e) => setEditForm({ ...editForm, expiryTime: e.target.value ? new Date(e.target.value).getTime() : 0 })}
/>
<Input
label="允许的域名 (可选)"
placeholder="example.com, panel.test.com"
description="限制使用此Token的来源面板域名,多个域名用逗号分隔,留空不限制"
value={editForm.allowedDomains}
onChange={(e) => setEditForm({ ...editForm, allowedDomains: e.target.value })}
/>
<Input
label="允许的API IP (可选)"
placeholder="203.0.113.10, 2001:db8::10, 198.51.100.0/24"
description="仅白名单IP可导入此分享,支持IPv4/IPv6/CIDR,多个用逗号分隔"
value={editForm.allowedIps}
onChange={(e) => setEditForm({ ...editForm, allowedIps: e.target.value })}
/>
</ModalBody>
<ModalFooter>
<Button onPress={() => setEditShareOpen(false)}>取消</Button>
<Button color="primary" onPress={handleEditShare}>保存</Button>
</ModalFooter>
</ModalContent>
</Modal>
{/* Import Node Modal */}
<Modal isOpen={importNodeOpen} onClose={() => setImportNodeOpen(false)}>
<ModalContent>