public key bug and multiuser enhancement and server ip support and config file of client optimization

pull/103/head
刘河 2019-03-26 23:34:55 +08:00
parent 00a4a33c5f
commit 42a73fa392
22 changed files with 155 additions and 70 deletions

View File

@ -200,20 +200,28 @@ func (s *Bridge) cliProcess(c *conn.Conn) {
func (s *Bridge) DelClient(id int) { func (s *Bridge) DelClient(id int) {
if v, ok := s.Client.Load(id); ok { if v, ok := s.Client.Load(id); ok {
if c, err := file.GetCsvDb().GetClient(id); err == nil && c.NoStore {
s.CloseClient <- c.Id
}
if v.(*Client).signal != nil { if v.(*Client).signal != nil {
v.(*Client).signal.Close() v.(*Client).signal.Close()
} }
s.Client.Delete(id) s.Client.Delete(id)
if file.GetCsvDb().IsPubClient(id) {
return
}
if c, err := file.GetCsvDb().GetClient(id); err == nil && c.NoStore {
s.CloseClient <- c.Id
}
} }
} }
//use different //use different
func (s *Bridge) typeDeal(typeVal string, c *conn.Conn, id int) { func (s *Bridge) typeDeal(typeVal string, c *conn.Conn, id int) {
isPub := file.GetCsvDb().IsPubClient(id)
switch typeVal { switch typeVal {
case common.WORK_MAIN: case common.WORK_MAIN:
if isPub {
c.Close()
return
}
//the vKey connect by another ,close the client of before //the vKey connect by another ,close the client of before
if v, ok := s.Client.LoadOrStore(id, NewClient(nil, nil, c)); ok { if v, ok := s.Client.LoadOrStore(id, NewClient(nil, nil, c)); ok {
if v.(*Client).signal != nil { if v.(*Client).signal != nil {
@ -229,16 +237,8 @@ func (s *Bridge) typeDeal(typeVal string, c *conn.Conn, id int) {
v.(*Client).tunnel = muxConn v.(*Client).tunnel = muxConn
} }
case common.WORK_CONFIG: case common.WORK_CONFIG:
var isPub bool
client, err := file.GetCsvDb().GetClient(id) client, err := file.GetCsvDb().GetClient(id)
if err == nil { if err != nil || (!isPub && !client.ConfigConnAllow) {
if client.VerifyKey == beego.AppConfig.String("public_vkey") {
isPub = true
} else {
isPub = false
}
}
if !isPub && !client.ConfigConnAllow {
c.Close() c.Close()
return return
} }
@ -458,6 +458,7 @@ loop:
tl := new(file.Tunnel) tl := new(file.Tunnel)
tl.Mode = t.Mode tl.Mode = t.Mode
tl.Port = ports[i] tl.Port = ports[i]
tl.ServerIp = t.ServerIp
if len(ports) == 1 { if len(ports) == 1 {
tl.Target = t.Target tl.Target = t.Target
tl.Remark = t.Remark tl.Remark = t.Remark

View File

@ -228,7 +228,13 @@ loop:
} }
func (s *TRPClient) Close() { func (s *TRPClient) Close() {
s.tunnel.Close() if s.tunnel != nil {
s.signal.Close() s.tunnel.Close()
s.ticker.Stop() }
if s.signal != nil {
s.signal.Close()
}
if s.ticker != nil {
s.ticker.Stop()
}
} }

View File

@ -1,7 +1,7 @@
[common] [common]
server=127.0.0.1:8024 server_addr=127.0.0.1:8024
tp=tcp conn_type=tcp
vkey=123 vkey=nps
auto_reconnection=true auto_reconnection=true
[health_check_test1] [health_check_test1]
@ -20,39 +20,38 @@ health_check_type=tcp
health_check_target=127.0.0.1:8083,127.0.0.1:8082 health_check_target=127.0.0.1:8083,127.0.0.1:8082
[web] [web]
host=b.o.com host=b.o.com
target=127.0.0.1:8080 target_addr=127.0.0.1:8080
[tcp] [tcp]
mode=tcp mode=tcp
target=127.0.0.1:8083,127.0.0.1:8082 target=127.0.0.1:8080
port=9006 server_port=10000
targetAddr=123.206.77.88
[socks5] [socks5]
mode=socks5 mode=socks5
port=9005 server_port=9005
[http] [http]
mode=httpProxy mode=httpProxy
port=9004 server_port=9004
[file] [file]
mode=file mode=file
port=9009 server_port=9009
local_path=./ local_path=./
strip_pre=/web/ strip_pre=/web/
[udp] [udp]
mode=udp mode=udp
port=53 server_port=53
target=114.114.114.114:53 target_addr=114.114.114.114:53
[secret_ssh] [secret_ssh]
port=2001 local_port=2001
password=sec password=sec
[p2p_ssh] [p2p_ssh]
port=2002 local_port=2002
password=c password=ppp
target=123.206.77.88:22 target_addr=192.168.74.199:22

View File

@ -6,6 +6,7 @@ runmode = pro
http_proxy_port=80 http_proxy_port=80
https_proxy_port=443 https_proxy_port=443
https_just_proxy=true https_just_proxy=true
http_proxy_ip=0.0.0.0
#certFile absolute path #certFile absolute path
#pem_path=conf/server.pem #pem_path=conf/server.pem
#KeyFile absolute path #KeyFile absolute path

View File

@ -1 +1 @@
9988,tcp,127.0.0.1:8080,1,3,11,,0,504, 9999,tcp,,1,3,11,,0,0,,0.0.0.0

1 9988 9999 tcp 127.0.0.1:8080 1 3 11 0 504 0 0.0.0.0

View File

@ -20,6 +20,7 @@ type CommonConfig struct {
type LocalServer struct { type LocalServer struct {
Type string Type string
Port int Port int
Ip string
Password string Password string
Target string Target string
} }
@ -112,11 +113,11 @@ func dealCommon(s string) *CommonConfig {
item = append(item, "") item = append(item, "")
} }
switch item[0] { switch item[0] {
case "server": case "server_addr":
c.Server = item[1] c.Server = item[1]
case "vkey": case "vkey":
c.VKey = item[1] c.VKey = item[1]
case "tp": case "conn_type":
c.Tp = item[1] c.Tp = item[1]
case "auto_reconnection": case "auto_reconnection":
c.AutoReconnection = common.GetBoolByStr(item[1]) c.AutoReconnection = common.GetBoolByStr(item[1])
@ -156,7 +157,7 @@ func dealHost(s string) *file.Host {
switch strings.TrimSpace(item[0]) { switch strings.TrimSpace(item[0]) {
case "host": case "host":
h.Host = item[1] h.Host = item[1]
case "target": case "target_addr":
h.Target = strings.Replace(item[1], ",", "\n", -1) h.Target = strings.Replace(item[1], ",", "\n", -1)
case "host_change": case "host_change":
h.HostChange = item[1] h.HostChange = item[1]
@ -211,13 +212,15 @@ func dealTunnel(s string) *file.Tunnel {
item = append(item, "") item = append(item, "")
} }
switch strings.TrimSpace(item[0]) { switch strings.TrimSpace(item[0]) {
case "port": case "server_port":
t.Ports = item[1] t.Ports = item[1]
case "server_ip":
t.ServerIp = item[1]
case "mode": case "mode":
t.Mode = item[1] t.Mode = item[1]
case "target": case "target_port", "target_addr":
t.Target = strings.Replace(item[1], ",", "\n", -1) t.Target = strings.Replace(item[1], ",", "\n", -1)
case "targetAddr": case "target_ip":
t.TargetAddr = item[1] t.TargetAddr = item[1]
case "password": case "password":
t.Password = item[1] t.Password = item[1]
@ -241,11 +244,13 @@ func delLocalService(s string) *LocalServer {
item = append(item, "") item = append(item, "")
} }
switch item[0] { switch item[0] {
case "port": case "local_port":
l.Port = common.GetIntNoErrByStr(item[1]) l.Port = common.GetIntNoErrByStr(item[1])
case "local_ip":
l.Ip = item[1]
case "password": case "password":
l.Password = item[1] l.Password = item[1]
case "target": case "target_addr":
l.Target = item[1] l.Target = item[1]
} }
} }

View File

@ -316,7 +316,7 @@ func (s *Conn) SendTaskInfo(t *file.Tunnel) (int, error) {
*/ */
raw := bytes.NewBuffer([]byte{}) raw := bytes.NewBuffer([]byte{})
binary.Write(raw, binary.LittleEndian, []byte(common.NEW_TASK)) binary.Write(raw, binary.LittleEndian, []byte(common.NEW_TASK))
common.BinaryWrite(raw, t.Mode, t.Ports, t.Target, t.Remark, t.TargetAddr, t.Password, t.LocalPath, t.StripPre) common.BinaryWrite(raw, t.Mode, t.Ports, t.Target, t.Remark, t.TargetAddr, t.Password, t.LocalPath, t.StripPre, t.ServerIp)
s.Lock() s.Lock()
defer s.Unlock() defer s.Unlock()
return s.Write(raw.Bytes()) return s.Write(raw.Bytes())
@ -345,6 +345,9 @@ func (s *Conn) GetTaskInfo() (t *file.Tunnel, err error) {
t.Password = arr[5] t.Password = arr[5]
t.LocalPath = arr[6] t.LocalPath = arr[6]
t.StripPre = arr[7] t.StripPre = arr[7]
if len(arr) > 8 {
t.ServerIp = arr[8]
}
t.NoStore = true t.NoStore = true
} }
return return

View File

@ -7,6 +7,7 @@ import (
"github.com/cnlh/nps/lib/common" "github.com/cnlh/nps/lib/common"
"github.com/cnlh/nps/lib/crypt" "github.com/cnlh/nps/lib/crypt"
"github.com/cnlh/nps/lib/rate" "github.com/cnlh/nps/lib/rate"
"github.com/cnlh/nps/vender/github.com/astaxie/beego"
"github.com/cnlh/nps/vender/github.com/astaxie/beego/logs" "github.com/cnlh/nps/vender/github.com/astaxie/beego/logs"
"net/http" "net/http"
"os" "os"
@ -59,6 +60,7 @@ func (s *Csv) StoreTasksToCsv() {
strconv.Itoa(int(task.Flow.ExportFlow)), strconv.Itoa(int(task.Flow.ExportFlow)),
strconv.Itoa(int(task.Flow.InletFlow)), strconv.Itoa(int(task.Flow.InletFlow)),
task.Password, task.Password,
task.ServerIp,
} }
err := writer.Write(record) err := writer.Write(record)
if err != nil { if err != nil {
@ -111,6 +113,11 @@ func (s *Csv) LoadTaskFromCsv() {
if post.Client, err = s.GetClient(common.GetIntNoErrByStr(item[5])); err != nil { if post.Client, err = s.GetClient(common.GetIntNoErrByStr(item[5])); err != nil {
continue continue
} }
if len(item) > 10 {
post.ServerIp = item[10]
} else {
post.ServerIp = "0.0.0.0"
}
s.Tasks.Store(post.Id, post) s.Tasks.Store(post.Id, post)
if post.Id > int(s.TaskIncreaseId) { if post.Id > int(s.TaskIncreaseId) {
s.TaskIncreaseId = int32(s.TaskIncreaseId) s.TaskIncreaseId = int32(s.TaskIncreaseId)
@ -440,7 +447,7 @@ func (s *Csv) UpdateClient(t *Client) error {
return nil return nil
} }
func (s *Csv) GetClientList(start, length int, search string) ([]*Client, int) { func (s *Csv) GetClientList(start, length int, search string, clientId int) ([]*Client, int) {
list := make([]*Client, 0) list := make([]*Client, 0)
var cnt int var cnt int
keys := common.GetMapKeys(s.Clients) keys := common.GetMapKeys(s.Clients)
@ -450,6 +457,9 @@ func (s *Csv) GetClientList(start, length int, search string) ([]*Client, int) {
if v.NoDisplay { if v.NoDisplay {
continue continue
} }
if clientId != 0 && clientId != v.Id {
continue
}
if search != "" && !(v.Id == common.GetIntNoErrByStr(search) || strings.Contains(v.VerifyKey, search) || strings.Contains(v.Remark, search)) { if search != "" && !(v.Id == common.GetIntNoErrByStr(search) || strings.Contains(v.VerifyKey, search) || strings.Contains(v.Remark, search)) {
continue continue
} }
@ -464,6 +474,18 @@ func (s *Csv) GetClientList(start, length int, search string) ([]*Client, int) {
return list, cnt return list, cnt
} }
func (s *Csv) IsPubClient(id int) bool {
client, err := s.GetClient(id)
if err == nil {
if client.VerifyKey == beego.AppConfig.String("public_vkey") {
return true
} else {
return false
}
}
return false
}
func (s *Csv) GetClient(id int) (c *Client, err error) { func (s *Csv) GetClient(id int) (c *Client, err error) {
if v, ok := s.Clients.Load(id); ok { if v, ok := s.Clients.Load(id); ok {
c = v.(*Client) c = v.(*Client)

View File

@ -111,8 +111,9 @@ func (s *Client) HasHost(h *Host) bool {
} }
type Tunnel struct { type Tunnel struct {
Id int //Id Id int //Id
Port int //服务端监听端口 Port int //服务端监听端口
ServerIp string
Mode string //启动方式 Mode string //启动方式
Target string //目标 Target string //目标
TargetArr []string //目标 TargetArr []string //目标

View File

@ -1,6 +1,6 @@
package version package version
const VERSION = "0.20.0" const VERSION = "0.20.1"
// Compulsory minimum version, Minimum downward compatibility to this version // Compulsory minimum version, Minimum downward compatibility to this version
func GetVersion() string { func GetVersion() string {

View File

@ -50,7 +50,7 @@ func GetHttpListener() (net.Listener, error) {
return pMux.GetHttpListener(), nil return pMux.GetHttpListener(), nil
} }
logs.Info("start http listener, port is", httpPort) logs.Info("start http listener, port is", httpPort)
return getTcpListener("", httpPort) return getTcpListener(beego.AppConfig.String("http_proxy_ip"), httpPort)
} }
func GetHttpsListener() (net.Listener, error) { func GetHttpsListener() (net.Listener, error) {
@ -59,7 +59,7 @@ func GetHttpsListener() (net.Listener, error) {
return pMux.GetHttpsListener(), nil return pMux.GetHttpsListener(), nil
} }
logs.Info("start https listener, port is", httpsPort) logs.Info("start https listener, port is", httpsPort)
return getTcpListener("", httpsPort) return getTcpListener(beego.AppConfig.String("http_proxy_ip"), httpsPort)
} }
func GetWebManagerListener() (net.Listener, error) { func GetWebManagerListener() (net.Listener, error) {

View File

@ -251,7 +251,7 @@ func (s *Sock5ModeServer) Auth(c net.Conn) error {
//start //start
func (s *Sock5ModeServer) Start() error { func (s *Sock5ModeServer) Start() error {
return conn.NewTcpListenerAndProcess(":"+strconv.Itoa(s.task.Port), func(c net.Conn) { return conn.NewTcpListenerAndProcess(s.task.ServerIp+":"+strconv.Itoa(s.task.Port), func(c net.Conn) {
if err := s.CheckFlowAndConnNum(s.task.Client); err != nil { if err := s.CheckFlowAndConnNum(s.task.Client); err != nil {
logs.Warn("client id %d, task id %d, error %s, when socks5 connection", s.task.Client.Id, s.task.Id, err.Error()) logs.Warn("client id %d, task id %d, error %s, when socks5 connection", s.task.Client.Id, s.task.Id, err.Error())
c.Close() c.Close()

View File

@ -32,7 +32,7 @@ func NewTunnelModeServer(process process, bridge *bridge.Bridge, task *file.Tunn
//开始 //开始
func (s *TunnelModeServer) Start() error { func (s *TunnelModeServer) Start() error {
return conn.NewTcpListenerAndProcess(":"+strconv.Itoa(s.task.Port), func(c net.Conn) { return conn.NewTcpListenerAndProcess(s.task.ServerIp+":"+strconv.Itoa(s.task.Port), func(c net.Conn) {
if err := s.CheckFlowAndConnNum(s.task.Client); err != nil { if err := s.CheckFlowAndConnNum(s.task.Client); err != nil {
logs.Warn("client id %d, task id %d,error %s, when tcp connection", s.task.Client.Id, s.task.Id, err.Error()) logs.Warn("client id %d, task id %d,error %s, when tcp connection", s.task.Client.Id, s.task.Id, err.Error())
c.Close() c.Close()

View File

@ -26,7 +26,10 @@ func NewUdpModeServer(bridge *bridge.Bridge, task *file.Tunnel) *UdpModeServer {
//开始 //开始
func (s *UdpModeServer) Start() error { func (s *UdpModeServer) Start() error {
var err error var err error
s.listener, err = net.ListenUDP("udp", &net.UDPAddr{net.ParseIP("0.0.0.0"), s.task.Port, ""}) if s.task.ServerIp == "" {
s.task.ServerIp = "0.0.0.0"
}
s.listener, err = net.ListenUDP("udp", &net.UDPAddr{net.ParseIP(s.task.ServerIp), s.task.Port, ""})
if err != nil { if err != nil {
return err return err
} }

View File

@ -255,8 +255,8 @@ func GetTunnel(start, length int, typeVal string, clientId int, search string) (
} }
//获取客户端列表 //获取客户端列表
func GetClientList(start, length int, search string) (list []*file.Client, cnt int) { func GetClientList(start, length int, search string, clientId int) (list []*file.Client, cnt int) {
list, cnt = file.GetCsvDb().GetClientList(start, length, search) list, cnt = file.GetCsvDb().GetClientList(start, length, search, clientId)
dealClientData() dealClientData()
return return
} }

View File

@ -140,7 +140,16 @@ func (s *BaseController) SetType(name string) {
func (s *BaseController) CheckUserAuth() { func (s *BaseController) CheckUserAuth() {
if s.controllerName == "client" { if s.controllerName == "client" {
s.StopRun() if s.actionName == "add" {
s.StopRun()
return
}
if id := s.GetIntNoErr("id"); id != 0 {
if id != s.GetSession("clientId").(int) {
s.StopRun()
return
}
}
} }
if s.controllerName == "index" { if s.controllerName == "index" {
if id := s.GetIntNoErr("id"); id != 0 { if id := s.GetIntNoErr("id"); id != 0 {

View File

@ -19,7 +19,14 @@ func (s *ClientController) List() {
return return
} }
start, length := s.GetAjaxParams() start, length := s.GetAjaxParams()
list, cnt := server.GetClientList(start, length, s.GetString("search")) clientIdSession := s.GetSession("clientId")
var clientId int
if clientIdSession == nil {
clientId = 0
} else {
clientId = clientIdSession.(int)
}
list, cnt := server.GetClientList(start, length, s.GetString("search"), clientId)
s.AjaxTable(list, cnt, cnt) s.AjaxTable(list, cnt, cnt)
} }

View File

@ -91,6 +91,7 @@ func (s *IndexController) Add() {
} else { } else {
t := &file.Tunnel{ t := &file.Tunnel{
Port: s.GetIntNoErr("port"), Port: s.GetIntNoErr("port"),
ServerIp: s.GetString("server_ip"),
Mode: s.GetString("type"), Mode: s.GetString("type"),
Target: s.GetString("target"), Target: s.GetString("target"),
Id: int(file.GetCsvDb().GetTaskId()), Id: int(file.GetCsvDb().GetTaskId()),
@ -144,7 +145,12 @@ func (s *IndexController) Edit() {
if t, err := file.GetCsvDb().GetTask(id); err != nil { if t, err := file.GetCsvDb().GetTask(id); err != nil {
s.error() s.error()
} else { } else {
t.Port = s.GetIntNoErr("port") var portChange bool
if s.GetIntNoErr("port") != t.Port {
portChange = true
t.Port = s.GetIntNoErr("port")
}
t.ServerIp = s.GetString("server_ip")
t.Mode = s.GetString("type") t.Mode = s.GetString("type")
t.Target = s.GetString("target") t.Target = s.GetString("target")
t.Password = s.GetString("password") t.Password = s.GetString("password")
@ -152,7 +158,7 @@ func (s *IndexController) Edit() {
t.LocalPath = s.GetString("local_path") t.LocalPath = s.GetString("local_path")
t.StripPre = s.GetString("strip_pre") t.StripPre = s.GetString("strip_pre")
t.Remark = s.GetString("remark") t.Remark = s.GetString("remark")
if !tool.TestServerPort(t.Port, t.Mode) { if portChange && !tool.TestServerPort(t.Port, t.Mode) {
s.AjaxErr("The port cannot be opened because it may has been occupied or is no longer allowed.") s.AjaxErr("The port cannot be opened because it may has been occupied or is no longer allowed.")
} }
if t.Client, err = file.GetCsvDb().GetClient(s.GetIntNoErr("client_id")); err != nil { if t.Client, err = file.GetCsvDb().GetClient(s.GetIntNoErr("client_id")); err != nil {

View File

@ -15,15 +15,18 @@
</a> </a>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<div class="table-responsive"> {{if eq true .isAdmin}}
<div id="toolbar">
<a href="/client/add" class="btn btn-primary dim" type="button" langtag="info-new"></a> <div class="table-responsive">
</div> <div id="toolbar">
<table id="taskList_table" class="table-striped table-hover" <a href="/client/add" class="btn btn-primary dim" type="button" langtag="info-new"></a>
data-mobile-responsive="true"></table>
</div> </div>
<table id="taskList_table" class="table-striped table-hover"
data-mobile-responsive="true"></table>
</div> </div>
</div>
{{end}}
<div class="ibox-content"> <div class="ibox-content">
<table id="table"></table> <table id="table"></table>
@ -225,11 +228,12 @@
sortable: true,//启用排序 sortable: true,//启用排序
formatter: function (value, row, index) { formatter: function (value, row, index) {
btn_group = '<div class="btn-group">' btn_group = '<div class="btn-group">'
btn = `<button onclick="del(` + row.Id + `)" class="btn-danger"><i class="fa fa-trash"></i></button><button onclick="edit(` + row.Id + `)" class="btn-primary"><i class="fa fa-edit"></i></button></div>` btn = ` {{if eq true .isAdmin}}<button onclick="del(` + row.Id + `)" class="btn-danger"><i class="fa fa-trash"></i></button>{{end}}<button onclick="edit(` + row.Id + `)" class="btn-primary"><i class="fa fa-edit"></i></button></div>`
if (row.Status) { if (row.Status) {
return btn_group + `<button onclick="stop(` + row.Id + `)" class="btn-warning"><i class="fa fa-close"></i></button>` + btn return btn_group {{if eq true .isAdmin}}+ `<button onclick="stop(` + row.Id + `)" class="btn-warning"><i class="fa fa-close"></i></button>` {{end}}+ btn
} else { } else {
return btn_group + `<button onclick="start(` + row.Id + `)" class="btn-warning"><i class="fa fa-check"></i></button>` + btn return btn_group {{if eq true .isAdmin}}+ `<button onclick="start(` + row.Id + `)" class="btn-warning"><i class="fa fa-check"></i></button>` {{end}}+ btn
} }
} }
}, },

View File

@ -28,6 +28,14 @@
</div> </div>
</div> </div>
<div class="form-group" id="server_ip">
<label class="col-sm-2 control-label" langtag="info-server-ip">ip</label>
<div class="col-sm-10">
<input class="form-control" type="text" value="0.0.0.0" name="server_ip"
placeholder="such as 0.0.0.0">
</div>
</div>
<div class="form-group" id="port"> <div class="form-group" id="port">
<label class="col-sm-2 control-label" langtag="info-server-port"></label> <label class="col-sm-2 control-label" langtag="info-server-port"></label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -28,7 +28,13 @@
placeholder="empty means to be unrestricted"> placeholder="empty means to be unrestricted">
</div> </div>
</div> </div>
<div class="form-group" id="server_ip">
<label class="col-sm-2 control-label" langtag="info-server-ip">ip</label>
<div class="col-sm-10">
<input class="form-control" type="text" value="{{.t.ServerIp}}" name="server_ip"
placeholder="such as 0.0.0.0">
</div>
</div>
<div class="form-group" id="port"> <div class="form-group" id="port">
<label class="col-sm-2 control-label" langtag="info-server-port"></label> <label class="col-sm-2 control-label" langtag="info-server-port"></label>
<div class="col-sm-10"> <div class="col-sm-10">

View File

@ -38,7 +38,13 @@
{{/*<img alt="image" class="img-circle" src="/static/img/profile_small.jpg"/>*/}} {{/*<img alt="image" class="img-circle" src="/static/img/profile_small.jpg"/>*/}}
</span> </span>
<a href="#"> <a href="#">
<span class="clear"> <span class="block m-t-xs"> <strong class="font-bold">admin</strong> <span class="clear"> <span class="block m-t-xs"> <strong class="font-bold">
{{if eq true .isAdmin}}
admin
{{else}}
user
{{end}}
</strong>
</span> <span class="text-muted text-xs block">system </span> </span> </span> <span class="text-muted text-xs block">system </span> </span>
</a> </a>
</div> </div>
@ -46,7 +52,6 @@
NPS NPS
</div> </div>
</li> </li>
{{if eq true .isAdmin}}
<li class="{{if eq "index" .menu}}active{{end}}"> <li class="{{if eq "index" .menu}}active{{end}}">
<a href="/"><i class="fa fa-dashboard"></i> <span langtag="menu-dashboard" <a href="/"><i class="fa fa-dashboard"></i> <span langtag="menu-dashboard"
class="nav-label"></span></a> class="nav-label"></span></a>
@ -55,7 +60,6 @@
<a href="/client/list"><i class="fa fa-clipboard"></i> <span langtag="menu-client" <a href="/client/list"><i class="fa fa-clipboard"></i> <span langtag="menu-client"
class="nav-label"></span></a> class="nav-label"></span></a>
</li> </li>
{{end}}
<li class="{{if eq "host" .menu}}active{{end}}"> <li class="{{if eq "host" .menu}}active{{end}}">
<a href="/index/hostlist"><i class="fa fa-paperclip"></i> <span langtag="menu-host" <a href="/index/hostlist"><i class="fa fa-paperclip"></i> <span langtag="menu-host"
class="nav-label"></span></a> class="nav-label"></span></a>