增加租户控制台启用禁用全局司南的能力

Signed-off-by: Chenyang Gao <gps949@outlook.com>
This commit is contained in:
Chenyang Gao
2023-04-11 14:35:31 +08:00
parent e44be54b89
commit 42c0fa839e
5 changed files with 287 additions and 11 deletions

View File

@@ -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

View File

@@ -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)

View File

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

View File

@@ -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
}
}
}

View File

@@ -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)