mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-29 07:56:37 +08:00
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:
@@ -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)
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user