mirror of
https://github.com/MirageNetwork/MirageServer.git
synced 2025-09-26 20:41:34 +08:00
@@ -92,6 +92,29 @@ function doRemoveNavi() {
|
||||
toastMsg.value = "已删除 " + selectNaviNode.value["HostName"];
|
||||
toastShow.value = true;
|
||||
NaviRegionList.value = response.data["data"]["Regions"];
|
||||
NaviDeplyPub.value = response.data["data"]["DeployPub"];
|
||||
BannedRegions.value = response.data["data"]["BannedRegions"];
|
||||
} else {
|
||||
toastMsg.value = response.data["status"].substring(6);
|
||||
toastShow.value = true;
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
toastMsg.value = error;
|
||||
toastShow.value = true;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleRegionBan(regionID) {
|
||||
axios
|
||||
.post("/admin/api/derp/ban/" + regionID, {})
|
||||
.then(function (response) {
|
||||
if (response.data["status"] == "success") {
|
||||
toastMsg.value = "已切换公共区域采用状态";
|
||||
toastShow.value = true;
|
||||
NaviRegionList.value = response.data["data"]["Regions"];
|
||||
NaviDeplyPub.value = response.data["data"]["DeployPub"];
|
||||
BannedRegions.value = response.data["data"]["BannedRegions"];
|
||||
} else {
|
||||
toastMsg.value = response.data["status"].substring(6);
|
||||
toastShow.value = true;
|
||||
@@ -105,6 +128,7 @@ function doRemoveNavi() {
|
||||
|
||||
//数据填充控制部分
|
||||
const NaviDeplyPub = ref("");
|
||||
const BannedRegions = ref([]);
|
||||
const NaviRegionList = ref([]);
|
||||
const NaviRegionNum = computed(() => {
|
||||
if (NaviRegionList.value == null) {
|
||||
@@ -127,6 +151,7 @@ function getNaviRegions() {
|
||||
// 处理成功情况
|
||||
NaviRegionList.value = response.data["data"]["Regions"];
|
||||
NaviDeplyPub.value = response.data["data"]["DeployPub"];
|
||||
BannedRegions.value = response.data["data"]["BannedRegions"];
|
||||
resolve();
|
||||
})
|
||||
.catch(function (error) {
|
||||
@@ -197,7 +222,160 @@ function secondsFormat(s) {
|
||||
</header>
|
||||
|
||||
<template v-for="nr in NaviRegionList">
|
||||
<table class="table w-full mb-3">
|
||||
<table v-if="nr.Region.OrgID == 0" class="table w-full mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
class="md:w-1/4 flex-auto md:flex-initial md:shrink-0 w-0 text-ellipsis pt-2 pb-1"
|
||||
>
|
||||
<div
|
||||
class="inline-flex items-center align-middle justify-center font-medium border border-stone-200 bg-stone-200 text-gray-600 rounded-full px-2 py-1 leading-none text-xs min-w-fit"
|
||||
>
|
||||
{{ nr.Region.RegionID }}# {{ nr.Region.RegionCode }}-{{
|
||||
nr.Region.RegionName + " "
|
||||
}}
|
||||
共 {{ nr.Nodes ? nr.Nodes.length : 0 }} 个
|
||||
<span
|
||||
class="ml-1 tooltip tooltip-top"
|
||||
:data-tip="
|
||||
BannedRegions.includes(nr.Region.RegionID) ? '点击启用' : '点击禁用'
|
||||
"
|
||||
>
|
||||
<div
|
||||
@click="toggleRegionBan(nr.Region.RegionID)"
|
||||
:class="{
|
||||
'border-red-50 bg-red-50 text-red-600': BannedRegions.includes(
|
||||
nr.Region.RegionID
|
||||
),
|
||||
'border-green-50 bg-green-50 text-green-600': !BannedRegions.includes(
|
||||
nr.Region.RegionID
|
||||
),
|
||||
}"
|
||||
class="inline-flex items-center align-middle justify-center font-medium border rounded-sm px-1 text-xs mr-1 cursor-pointer"
|
||||
>
|
||||
全局
|
||||
</div>
|
||||
</span>
|
||||
</div>
|
||||
</th>
|
||||
<th class="hidden md:table-cell md:w-1/4 pt-2 pb-1">IP</th>
|
||||
<th class="hidden md:table-cell w-1/4 lg:w-1/5 pt-2 pb-1">端口</th>
|
||||
<th class="hidden lg:table-cell md:flex-auto pt-2 pb-1">状态</th>
|
||||
<th
|
||||
class="table-cell justify-end ml-auto md:ml-0 relative w-1/6 lg:w-12 pt-2 pb-1"
|
||||
>
|
||||
<span class="sr-only">司南操作菜单</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="nn in nr.Nodes">
|
||||
<tr
|
||||
:v-if="nn != nil"
|
||||
@mouseenter="mouseOnNaviNode(nn)"
|
||||
@mouseleave="mouseLeaveNaviNode()"
|
||||
class="w-full px-0.5 hover"
|
||||
>
|
||||
<td
|
||||
class="md:w-1/4 flex-auto md:flex-initial md:shrink-0 w-0 text-ellipsis"
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="items-center text-gray-900">
|
||||
<p class="font-semibold hover:text-blue-500">
|
||||
<span
|
||||
:class="{
|
||||
'bg-green-500': nn.Statics.latency != -1,
|
||||
'bg-gray-300': nn.Statics.latency == -1,
|
||||
}"
|
||||
class="inline-block w-2 h-2 rounded-full relative -top-px lg:hidden mr-2"
|
||||
></span>
|
||||
<a class="stretched-link">{{ nn.HostName }} </a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="md:hidden flex space-x-1 truncate">
|
||||
<span class="text-sm">{{
|
||||
nn.Statics.latency != -1 ? nn.Statics.latency + "ms" : "断开"
|
||||
}}</span
|
||||
><span>·</span
|
||||
><span class="md:hidden text-gray-600 text-sm">{{
|
||||
nn.NoDERP ? "无中继" : "中继" + nn.DERPPort
|
||||
}}</span
|
||||
><span>·</span
|
||||
><span class="md:hidden text-gray-600 text-sm">{{
|
||||
nn.NoSTUN ? "无导航" : "导航" + nn.STUNPort
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="flex items-center text-gray-600 text-xs">
|
||||
<span>{{ nn.Name }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden md:table-cell md:w-1/4">
|
||||
<div class="flex relative min-w-0">
|
||||
<div class="flex flex-col items-start text-gray-600 text-sm">
|
||||
<span>IPv4: {{ nn.IPv4 == "" ? "未指定" : nn.IPv4 }} </span>
|
||||
<span>IPv6: {{ nn.IPv6 == "" ? "未指定" : nn.IPv6 }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden md:table-cell w-1/4 lg:w-1/5">
|
||||
<div class="flex relative min-w-0">
|
||||
<div class="flex flex-col items-start text-sm">
|
||||
<span>中继: {{ nn.NoDERP ? "已禁用" : nn.DERPPort }}</span>
|
||||
<span>导航: {{ nn.NoSTUN ? "已禁用" : nn.STUNPort }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="hidden lg:table-cell md:flex-auto">
|
||||
<span>
|
||||
<div class="inline-flex items-center cursor-default">
|
||||
<span
|
||||
class="inline-block w-2 h-2 rounded-full mr-2"
|
||||
:class="{
|
||||
'bg-green-500': nn.Statics.latency != -1,
|
||||
'bg-gray-300': nn.Statics.latency == -1,
|
||||
}"
|
||||
></span>
|
||||
<span class="text-sm text-gray-600">
|
||||
{{
|
||||
nn.Statics.latency != -1 ? nn.Statics.latency + "ms" : "断开"
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
<td class="table-cell justify-end ml-auto md:ml-0 relative w-1/6 lg:w-12">
|
||||
<div class="flex-none w-12 -mt-0.5 relative">
|
||||
<div
|
||||
class="py-0.5 px-2 shadow-none rounded-md border border-gray-300/0 transition-shadow duration-100 ease-in-out z-20"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="none"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="text-gray-500"
|
||||
>
|
||||
<circle cx="12" cy="12" r="1"></circle>
|
||||
<circle cx="19" cy="12" r="1"></circle>
|
||||
<circle cx="5" cy="12" r="1"></circle>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template v-for="nr in NaviRegionList">
|
||||
<table v-if="nr.Region.OrgID != 0" class="table w-full mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th
|
||||
|
@@ -362,7 +362,7 @@ func (h *Mirage) initRouter(router *mux.Router) {
|
||||
console_router.HandleFunc("/api/keys", h.CAPIGetKeys).Methods(http.MethodGet)
|
||||
console_router.HandleFunc("/api/acls/tags", h.CAPIGetTags).Methods(http.MethodGet)
|
||||
console_router.HandleFunc("/api/subscription", h.CAPIGetSubscription).Methods(http.MethodGet)
|
||||
console_router.HandleFunc("/api/derp/add", h.CAPIAddDERP).Methods(http.MethodPost)
|
||||
console_router.HandleFunc("/api/derp/query", h.CAPIQueryDERP).Methods(http.MethodGet)
|
||||
|
||||
// POST(更新类)API
|
||||
console_router.HandleFunc("/api/users", h.CAPIPostUsers).Methods(http.MethodPost)
|
||||
@@ -373,7 +373,8 @@ func (h *Mirage) initRouter(router *mux.Router) {
|
||||
console_router.HandleFunc("/api/acls/tags", h.CAPIPostTags).Methods(http.MethodPost)
|
||||
console_router.HandleFunc("/api/dns", h.CAPIPostDNS).Methods(http.MethodPost)
|
||||
console_router.HandleFunc("/api/tcd", h.CAPIPostTCD).Methods(http.MethodPost)
|
||||
console_router.HandleFunc("/api/derp/query", h.CAPIQueryDERP).Methods(http.MethodGet)
|
||||
console_router.HandleFunc("/api/derp/add", h.CAPIAddDERP).Methods(http.MethodPost)
|
||||
console_router.HandleFunc("/api/derp/ban/{id}", h.CAPISwitchRegionBan).Methods(http.MethodPost)
|
||||
|
||||
// DELETE(删除类)API
|
||||
console_router.PathPrefix("/api/keys/").HandlerFunc(h.CAPIDelKeys).Methods(http.MethodDelete)
|
||||
|
@@ -8,6 +8,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -17,8 +18,9 @@ import (
|
||||
)
|
||||
|
||||
type NaviQueryRes struct {
|
||||
DeployPub string `json:"DeployPub"`
|
||||
Regions []NaviQueryRegion `json:"Regions"`
|
||||
DeployPub string `json:"DeployPub"`
|
||||
BannedRegions []int `json:"BannedRegions"`
|
||||
Regions []NaviQueryRegion `json:"Regions"`
|
||||
}
|
||||
type NaviQueryRegion struct {
|
||||
Region NaviRegion `json:"Region"`
|
||||
@@ -82,6 +84,12 @@ func (m *Mirage) CAPIQueryDERP(
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
resData.BannedRegions = []int{}
|
||||
for id := range org.NaviBanList {
|
||||
resData.BannedRegions = append(resData.BannedRegions, id)
|
||||
}
|
||||
|
||||
resData.DeployPub = org.NaviDeployPub
|
||||
|
||||
m.doAPIResponse(w, "", resData)
|
||||
@@ -136,6 +144,9 @@ func (m *Mirage) CAPIAddDERP(
|
||||
m.doAPIResponse(w, "新建司南档案失败", nil)
|
||||
return
|
||||
}
|
||||
|
||||
m.setOrgLastStateChangeToNow(user.OrganizationID)
|
||||
|
||||
m.CAPIQueryDERP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -330,6 +341,8 @@ WantedBy=multi-user.target`
|
||||
return
|
||||
}
|
||||
|
||||
m.setOrgLastStateChangeToNow(user.OrganizationID)
|
||||
|
||||
m.CAPIQueryDERP(w, r)
|
||||
}
|
||||
|
||||
@@ -369,5 +382,59 @@ func (m *Mirage) CAPIDelNaviNode(
|
||||
delete(m.DERPseqnum, naviID)
|
||||
// c.App.LoadDERPMapFromURL(c.App.cfg.DERPURL)
|
||||
|
||||
m.setOrgLastStateChangeToNow(user.OrganizationID)
|
||||
|
||||
m.CAPIQueryDERP(w, r)
|
||||
}
|
||||
|
||||
func (m *Mirage) CAPISwitchRegionBan(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
) {
|
||||
user, err := m.verifyTokenIDandGetUser(w, r)
|
||||
if err != nil || user.CheckEmpty() {
|
||||
m.doAPIResponse(w, "用户信息核对失败:"+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
regionIDStr, ok := vars["id"]
|
||||
if !ok {
|
||||
m.doAPIResponse(w, "未指定区域ID", nil)
|
||||
return
|
||||
}
|
||||
regionID, err := strconv.Atoi(regionIDStr)
|
||||
if err != nil {
|
||||
m.doAPIResponse(w, "区域ID格式错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
region := m.GetNaviRegion(regionID)
|
||||
if region == nil || region.OrgID != 0 {
|
||||
m.doAPIResponse(w, "未找到该区域档案", nil)
|
||||
return
|
||||
}
|
||||
|
||||
org, err := m.GetOrgnaizationByID(user.OrganizationID)
|
||||
if err != nil {
|
||||
m.doAPIResponse(w, "查询用户所属组织失败", nil)
|
||||
return
|
||||
}
|
||||
if _, ok := org.NaviBanList[regionID]; ok {
|
||||
delete(org.NaviBanList, regionID)
|
||||
} else {
|
||||
if org.NaviBanList == nil {
|
||||
org.NaviBanList = make(map[int]struct{})
|
||||
}
|
||||
org.NaviBanList[regionID] = struct{}{}
|
||||
}
|
||||
|
||||
if err := m.db.Save(org).Error; err != nil {
|
||||
m.doAPIResponse(w, "数据库更新组织禁用区域信息失败:"+err.Error(), nil)
|
||||
return
|
||||
}
|
||||
|
||||
m.setOrgLastStateChangeToNow(org.ID)
|
||||
|
||||
m.CAPIQueryDERP(w, r)
|
||||
}
|
||||
|
@@ -209,17 +209,25 @@ func (m *Mirage) LoadOrgDERPs(orgID int64) (*tailcfg.DERPMap, error) {
|
||||
Regions: make(map[int]*tailcfg.DERPRegion),
|
||||
}
|
||||
|
||||
org, err := m.GetOrgnaizationByID(orgID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get organization")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 从数据库读取DERP信息
|
||||
naviRegions := m.ListNaviRegions()
|
||||
if len(naviRegions) != 0 {
|
||||
for _, nr := range naviRegions {
|
||||
if nr.OrgID == 0 || nr.OrgID == orgID {
|
||||
derpRegion, err := m.toDERPRegion(nr)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot convert NaviRegion to DERPRegion")
|
||||
return nil, err
|
||||
if _, ok := org.NaviBanList[nr.ID]; !ok {
|
||||
if nr.OrgID == 0 || nr.OrgID == orgID {
|
||||
derpRegion, err := m.toDERPRegion(nr)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot convert NaviRegion to DERPRegion")
|
||||
return nil, err
|
||||
}
|
||||
derpMap.Regions[derpRegion.RegionID] = &derpRegion
|
||||
}
|
||||
derpMap.Regions[derpRegion.RegionID] = &derpRegion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,10 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -36,6 +39,7 @@ type Organization struct {
|
||||
AclPolicy *ACLPolicy
|
||||
AclRules []tailcfg.FilterRule `gorm:"-"`
|
||||
SshPolicy *tailcfg.SSHPolicy `gorm:"-"`
|
||||
NaviBanList NaviBanList
|
||||
NaviDeployKey string
|
||||
NaviDeployPub string
|
||||
|
||||
@@ -43,6 +47,24 @@ type Organization struct {
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
type NaviBanList map[int]struct{}
|
||||
|
||||
func (nbl NaviBanList) Value() (driver.Value, error) {
|
||||
b, err := json.Marshal(nbl)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
func (nbl *NaviBanList) Scan(value interface{}) error {
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
return json.Unmarshal(v, nbl)
|
||||
case string:
|
||||
return json.Unmarshal([]byte(v), nbl)
|
||||
default:
|
||||
return fmt.Errorf("cannot parse admin credential: unexpected data type %T", value)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Organization) BeforeCreate(tx *gorm.DB) error {
|
||||
if o.ID == 0 {
|
||||
flakeID, err := snowflake.NewNode(1)
|
||||
|
Reference in New Issue
Block a user