mirror of
https://github.com/vx3r/wg-gen-web.git
synced 2024-11-23 02:42:07 +01:00
commit
532587ff62
5
.env
5
.env
@ -39,3 +39,8 @@ OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr
|
|||||||
|
|
||||||
# set provider name to fake to disable auth, also the default
|
# set provider name to fake to disable auth, also the default
|
||||||
OAUTH2_PROVIDER_NAME=fake
|
OAUTH2_PROVIDER_NAME=fake
|
||||||
|
|
||||||
|
# https://github.com/jamescun/wg-api integration, user and password (basic auth) are optional
|
||||||
|
WG_STATS_API=
|
||||||
|
WG_STATS_API_USER=
|
||||||
|
WG_STATS_API_PASS=
|
24
README.md
24
README.md
@ -46,7 +46,7 @@ The easiest way to run Wg Gen Web is using the container image
|
|||||||
```
|
```
|
||||||
docker run --rm -it -v /tmp/wireguard:/data -p 8080:8080 -e "WG_CONF_DIR=/data" vx3r/wg-gen-web:latest
|
docker run --rm -it -v /tmp/wireguard:/data -p 8080:8080 -e "WG_CONF_DIR=/data" vx3r/wg-gen-web:latest
|
||||||
```
|
```
|
||||||
Docker compose snippet, used for demo server
|
Docker compose snippet, used for demo server, wg-json-api service is optional
|
||||||
```
|
```
|
||||||
version: '3.6'
|
version: '3.6'
|
||||||
wg-gen-web-demo:
|
wg-gen-web-demo:
|
||||||
@ -70,6 +70,14 @@ version: '3.6'
|
|||||||
- OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr
|
- OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/wireguard:/data
|
- /etc/wireguard:/data
|
||||||
|
wg-json-api:
|
||||||
|
image: james/wg-api:latest
|
||||||
|
container_name: wg-json-api
|
||||||
|
restart: unless-stopped
|
||||||
|
cap_add:
|
||||||
|
- NET_ADMIN
|
||||||
|
network_mode: "host"
|
||||||
|
command: wg-api --device wg0 --listen localhost:8182
|
||||||
```
|
```
|
||||||
Please note that mapping ```/etc/wireguard``` to ```/data``` inside the docker, will erase your host's current configuration.
|
Please note that mapping ```/etc/wireguard``` to ```/data``` inside the docker, will erase your host's current configuration.
|
||||||
If needed, please make sure to backup your files from ```/etc/wireguard```.
|
If needed, please make sure to backup your files from ```/etc/wireguard```.
|
||||||
@ -177,9 +185,21 @@ OAUTH2_CLIENT_SECRET=********************
|
|||||||
OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr
|
OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr
|
||||||
```
|
```
|
||||||
|
|
||||||
Please fell free to test and report any bugs.
|
|
||||||
Wg Gen Web will only access your profile to get email address and your name, no other unnecessary scopes will be requested.
|
Wg Gen Web will only access your profile to get email address and your name, no other unnecessary scopes will be requested.
|
||||||
|
|
||||||
|
## WireGuard Status Display
|
||||||
|
Wg Gen Web integrates a [WireGuard API implementation](https://github.com/jamescun/wg-api) to display client stats.
|
||||||
|
In order to enable the Status API integration, the following settings need to be configured:
|
||||||
|
```
|
||||||
|
# https://github.com/jamescun/wg-api integration, user and password (basic auth) are optional
|
||||||
|
WG_STATS_API=http://localhost:8182
|
||||||
|
WG_STATS_API_USER=
|
||||||
|
WG_STATS_API_PASS=
|
||||||
|
```
|
||||||
|
To setup the WireGuard API take a look at [https://github.com/jamescun/wg-api/blob/master/README.md](https://github.com/jamescun/wg-api/blob/master/README.md).
|
||||||
|
|
||||||
|
Please fell free to test and report any bugs.
|
||||||
|
|
||||||
## Need Help
|
## Need Help
|
||||||
|
|
||||||
* Join us on [Discord](https://discord.gg/fjx7gGJ)
|
* Join us on [Discord](https://discord.gg/fjx7gGJ)
|
||||||
|
50
api/v1/status/status.go
Normal file
50
api/v1/status/status.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplyRoutes applies router to gin Router
|
||||||
|
func ApplyRoutes(r *gin.RouterGroup) {
|
||||||
|
g := r.Group("/status")
|
||||||
|
{
|
||||||
|
g.GET("/enabled", readEnabled)
|
||||||
|
g.GET("/interface", readInterfaceStatus)
|
||||||
|
g.GET("/clients", readClientStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readEnabled(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, os.Getenv("WG_STATS_API") != "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func readInterfaceStatus(c *gin.Context) {
|
||||||
|
status, err := core.ReadInterfaceStatus()
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to read interface status")
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, status)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readClientStatus(c *gin.Context) {
|
||||||
|
status, err := core.ReadClientStatus()
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"err": err,
|
||||||
|
}).Error("failed to read client status")
|
||||||
|
c.AbortWithStatusJSON(http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, status)
|
||||||
|
}
|
@ -5,6 +5,7 @@ import (
|
|||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/auth"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/auth"
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/client"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/client"
|
||||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/server"
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/server"
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ApplyRoutes apply routes to gin router
|
// ApplyRoutes apply routes to gin router
|
||||||
@ -14,9 +15,9 @@ func ApplyRoutes(r *gin.RouterGroup, private bool) {
|
|||||||
if private {
|
if private {
|
||||||
client.ApplyRoutes(v1)
|
client.ApplyRoutes(v1)
|
||||||
server.ApplyRoutes(v1)
|
server.ApplyRoutes(v1)
|
||||||
|
status.ApplyRoutes(v1)
|
||||||
} else {
|
} else {
|
||||||
auth.ApplyRoutes(v1)
|
auth.ApplyRoutes(v1)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
182
core/status.go
Normal file
182
core/status.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// apiError implements a top-level JSON-RPC error.
|
||||||
|
type apiError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
|
||||||
|
Data interface{} `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiRequest struct {
|
||||||
|
Version string `json:"jsonrpc"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
Params json.RawMessage `json:"params,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type apiResponse struct {
|
||||||
|
Version string `json:"jsonrpc"`
|
||||||
|
Result interface{} `json:"result,omitempty"`
|
||||||
|
Error *apiError `json:"error,omitempty"`
|
||||||
|
ID json.RawMessage `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWireGuardAPI(reqData apiRequest) (*apiResponse, error) {
|
||||||
|
apiUrl := os.Getenv("WG_STATS_API")
|
||||||
|
if apiUrl == "" {
|
||||||
|
return nil, errors.New("Status API integration not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiClient := http.Client{
|
||||||
|
Timeout: time.Second * 2, // Timeout after 2 seconds
|
||||||
|
}
|
||||||
|
jsonData, _ := json.Marshal(reqData)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, apiUrl, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "wg-gen-web")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Cache-Control", "no-cache")
|
||||||
|
|
||||||
|
if os.Getenv("WG_STATS_API_USER") != "" {
|
||||||
|
req.SetBasicAuth(os.Getenv("WG_STATS_API_USER"), os.Getenv("WG_STATS_API_PASS"))
|
||||||
|
}
|
||||||
|
|
||||||
|
res, getErr := apiClient.Do(req)
|
||||||
|
if getErr != nil {
|
||||||
|
return nil, getErr
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Body != nil {
|
||||||
|
defer res.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
body, readErr := ioutil.ReadAll(res.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
response := apiResponse{}
|
||||||
|
jsonErr := json.Unmarshal(body, &response)
|
||||||
|
if jsonErr != nil {
|
||||||
|
return nil, jsonErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return &response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadInterfaceStatus object, create default one
|
||||||
|
func ReadInterfaceStatus() (*model.InterfaceStatus, error) {
|
||||||
|
interfaceStatus := &model.InterfaceStatus{
|
||||||
|
Name: "unknown",
|
||||||
|
DeviceType: "unknown",
|
||||||
|
ListenPort: 0,
|
||||||
|
NumberOfPeers: 0,
|
||||||
|
PublicKey: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := fetchWireGuardAPI(apiRequest{
|
||||||
|
Version: "2.0",
|
||||||
|
Method: "GetDeviceInfo",
|
||||||
|
Params: nil,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return interfaceStatus, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resultData := data.Result.(map[string]interface{})
|
||||||
|
device := resultData["device"].(map[string]interface{})
|
||||||
|
interfaceStatus.Name = device["name"].(string)
|
||||||
|
interfaceStatus.DeviceType = device["type"].(string)
|
||||||
|
interfaceStatus.PublicKey = device["public_key"].(string)
|
||||||
|
interfaceStatus.ListenPort = int(device["listen_port"].(float64))
|
||||||
|
interfaceStatus.NumberOfPeers = int(device["num_peers"].(float64))
|
||||||
|
|
||||||
|
return interfaceStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadClientStatus object, create default one, last recent active client is listed first
|
||||||
|
func ReadClientStatus() ([]*model.ClientStatus, error) {
|
||||||
|
var clientStatus []*model.ClientStatus
|
||||||
|
|
||||||
|
data, err := fetchWireGuardAPI(apiRequest{
|
||||||
|
Version: "2.0",
|
||||||
|
Method: "ListPeers",
|
||||||
|
Params: []byte("{}"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return clientStatus, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resultData := data.Result.(map[string]interface{})
|
||||||
|
peers := resultData["peers"].([]interface{})
|
||||||
|
|
||||||
|
clients, err := ReadClients()
|
||||||
|
withClientDetails := true
|
||||||
|
if err != nil {
|
||||||
|
withClientDetails = false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tmpPeer := range peers {
|
||||||
|
peer := tmpPeer.(map[string]interface{})
|
||||||
|
peerHandshake, _ := time.Parse(time.RFC3339Nano, peer["last_handshake"].(string))
|
||||||
|
peerIPs := peer["allowed_ips"].([]interface{})
|
||||||
|
peerAddresses := make([]string, len(peerIPs))
|
||||||
|
for i, peerIP := range peerIPs {
|
||||||
|
peerAddresses[i] = peerIP.(string)
|
||||||
|
}
|
||||||
|
peerHandshakeRelative := time.Since(peerHandshake)
|
||||||
|
peerActive := peerHandshakeRelative.Minutes() < 3 // TODO: we need a better detection... ping for example?
|
||||||
|
|
||||||
|
newClientStatus := &model.ClientStatus{
|
||||||
|
PublicKey: peer["public_key"].(string),
|
||||||
|
HasPresharedKey: peer["has_preshared_key"].(bool),
|
||||||
|
ProtocolVersion: int(peer["protocol_version"].(float64)),
|
||||||
|
Name: "UNKNOWN",
|
||||||
|
Email: "UNKNOWN",
|
||||||
|
Connected: peerActive,
|
||||||
|
AllowedIPs: peerAddresses,
|
||||||
|
Endpoint: peer["endpoint"].(string),
|
||||||
|
LastHandshake: peerHandshake,
|
||||||
|
LastHandshakeRelative: peerHandshakeRelative,
|
||||||
|
ReceivedBytes: int(peer["receive_bytes"].(float64)),
|
||||||
|
TransmittedBytes: int(peer["transmit_bytes"].(float64)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if withClientDetails {
|
||||||
|
for _, client := range clients {
|
||||||
|
if client.PublicKey != newClientStatus.PublicKey {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
newClientStatus.Name = client.Name
|
||||||
|
newClientStatus.Email = client.Email
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clientStatus = append(clientStatus, newClientStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(clientStatus, func(i, j int) bool {
|
||||||
|
return clientStatus[i].LastHandshakeRelative < clientStatus[j].LastHandshakeRelative
|
||||||
|
})
|
||||||
|
|
||||||
|
return clientStatus, nil
|
||||||
|
}
|
67
model/status.go
Normal file
67
model/status.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ClientStatus structure
|
||||||
|
type ClientStatus struct {
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
HasPresharedKey bool `json:"hasPresharedKey"`
|
||||||
|
ProtocolVersion int `json:"protocolVersion"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Connected bool `json:"connected"`
|
||||||
|
AllowedIPs []string `json:"allowedIPs"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
LastHandshake time.Time `json:"lastHandshake"`
|
||||||
|
LastHandshakeRelative time.Duration `json:"lastHandshakeRelative"`
|
||||||
|
ReceivedBytes int `json:"receivedBytes"`
|
||||||
|
TransmittedBytes int `json:"transmittedBytes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientStatus) MarshalJSON() ([]byte, error) {
|
||||||
|
|
||||||
|
duration := fmt.Sprintf("%v ago", c.LastHandshakeRelative)
|
||||||
|
if c.LastHandshakeRelative.Hours() > 5208 { // 24*7*31 = approx one month
|
||||||
|
duration = "more than a month ago"
|
||||||
|
}
|
||||||
|
return json.Marshal(&struct {
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
HasPresharedKey bool `json:"hasPresharedKey"`
|
||||||
|
ProtocolVersion int `json:"protocolVersion"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Connected bool `json:"connected"`
|
||||||
|
AllowedIPs []string `json:"allowedIPs"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
LastHandshake time.Time `json:"lastHandshake"`
|
||||||
|
LastHandshakeRelative string `json:"lastHandshakeRelative"`
|
||||||
|
ReceivedBytes int `json:"receivedBytes"`
|
||||||
|
TransmittedBytes int `json:"transmittedBytes"`
|
||||||
|
}{
|
||||||
|
PublicKey: c.PublicKey,
|
||||||
|
HasPresharedKey: c.HasPresharedKey,
|
||||||
|
ProtocolVersion: c.ProtocolVersion,
|
||||||
|
Name: c.Name,
|
||||||
|
Email: c.Email,
|
||||||
|
Connected: c.Connected,
|
||||||
|
AllowedIPs: c.AllowedIPs,
|
||||||
|
Endpoint: c.Endpoint,
|
||||||
|
LastHandshake: c.LastHandshake,
|
||||||
|
LastHandshakeRelative: duration,
|
||||||
|
ReceivedBytes: c.ReceivedBytes,
|
||||||
|
TransmittedBytes: c.TransmittedBytes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceStatus structure
|
||||||
|
type InterfaceStatus struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
DeviceType string `json:"type"`
|
||||||
|
ListenPort int `json:"listenPort"`
|
||||||
|
NumberOfPeers int `json:"numPeers"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
}
|
@ -15,6 +15,10 @@
|
|||||||
Server
|
Server
|
||||||
<v-icon right dark>mdi-vpn</v-icon>
|
<v-icon right dark>mdi-vpn</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn to="/status">
|
||||||
|
Status
|
||||||
|
<v-icon right dark>mdi-chart-bar</v-icon>
|
||||||
|
</v-btn>
|
||||||
</v-toolbar-items>
|
</v-toolbar-items>
|
||||||
|
|
||||||
<v-menu
|
<v-menu
|
||||||
|
170
ui/src/components/Status.vue
Normal file
170
ui/src/components/Status.vue
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<template>
|
||||||
|
<v-container>
|
||||||
|
<v-row v-if="dataLoaded">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
WireGuard Interface Status: {{ interface.name }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-list-item>
|
||||||
|
<v-list-item-content>
|
||||||
|
<v-list-item-subtitle>Public Key: {{ interface.publicKey }}</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>Listening Port: {{ interface.listenPort }}</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>Device Type: {{ interface.type }}</v-list-item-subtitle>
|
||||||
|
<v-list-item-subtitle>Number of Peers: {{ interface.numPeers }}</v-list-item-subtitle>
|
||||||
|
</v-list-item-content>
|
||||||
|
</v-list-item>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="dataLoaded">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
WireGuard Client Status
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-text-field
|
||||||
|
v-model="search"
|
||||||
|
append-icon="mdi-magnify"
|
||||||
|
label="Search"
|
||||||
|
single-line
|
||||||
|
hide-details
|
||||||
|
></v-text-field>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="success"
|
||||||
|
@click="reload"
|
||||||
|
>
|
||||||
|
Reload
|
||||||
|
<v-icon right dark>mdi-reload</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-card-title>
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="clients"
|
||||||
|
:search="search"
|
||||||
|
>
|
||||||
|
<template v-slot:item.connected="{ item }">
|
||||||
|
<v-icon left v-if="item.connected" color="success">mdi-lan-connect</v-icon>
|
||||||
|
<v-icon left v-else>mdi-lan-disconnect</v-icon>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.receivedBytes="{ item }">
|
||||||
|
{{ humanFileSize(item.receivedBytes) }}
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.transmittedBytes="{ item }">
|
||||||
|
{{ humanFileSize(item.transmittedBytes) }}
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.allowedIPs="{ item }">
|
||||||
|
<v-chip
|
||||||
|
v-for="(ip, i) in item.allowedIPs"
|
||||||
|
:key="i"
|
||||||
|
color="indigo"
|
||||||
|
text-color="white"
|
||||||
|
>
|
||||||
|
<v-icon left>mdi-ip-network</v-icon>
|
||||||
|
{{ ip }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
<template v-slot:item.lastHandshake="{ item }">
|
||||||
|
<v-row>
|
||||||
|
<p>{{ item.lastHandshake | formatDate }} ({{ item.lastHandshakeRelative }})</p>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-else>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>
|
||||||
|
No stats available...
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text v-if="enabled">{{ error }}</v-card-text>
|
||||||
|
<v-card-text v-else>Status API integration not configured.</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Status',
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
search: '',
|
||||||
|
headers: [
|
||||||
|
{ text: 'Connected', value: 'connected', },
|
||||||
|
{ text: 'Name', value: 'name', },
|
||||||
|
{ text: 'Endpoint', value: 'endpoint', },
|
||||||
|
{ text: 'IP addresses', value: 'allowedIPs', sortable: false, },
|
||||||
|
{ text: 'Received Bytes', value: 'receivedBytes', },
|
||||||
|
{ text: 'Transmitted Bytes', value: 'transmittedBytes', },
|
||||||
|
{ text: 'Last Handshake', value: 'lastHandshake',} ,
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
|
computed:{
|
||||||
|
...mapGetters({
|
||||||
|
interface: 'status/interfaceStatus',
|
||||||
|
clients: 'status/clientStatus',
|
||||||
|
enabled: 'status/enabled',
|
||||||
|
error: 'status/error',
|
||||||
|
}),
|
||||||
|
dataLoaded: function () {
|
||||||
|
return this.enabled && this.interface != null && this.interface.name !== "";
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted () {
|
||||||
|
this.readEnabled()
|
||||||
|
if(this.enabled) {
|
||||||
|
this.readStatus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
enabled(newValue, oldValue) {
|
||||||
|
if (this.enabled) {
|
||||||
|
this.readStatus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
...mapActions('status', {
|
||||||
|
readStatus: 'read',
|
||||||
|
readEnabled: 'isEnabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
reload() {
|
||||||
|
this.readStatus()
|
||||||
|
},
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string
|
||||||
|
humanFileSize(bytes, si=false, dp=1) {
|
||||||
|
const thresh = si ? 1000 : 1024;
|
||||||
|
|
||||||
|
if (Math.abs(bytes) < thresh) {
|
||||||
|
return bytes + ' B';
|
||||||
|
}
|
||||||
|
|
||||||
|
const units = si
|
||||||
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
|
let u = -1;
|
||||||
|
const r = 10**dp;
|
||||||
|
|
||||||
|
do {
|
||||||
|
bytes /= thresh;
|
||||||
|
++u;
|
||||||
|
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||||
|
|
||||||
|
|
||||||
|
return bytes.toFixed(dp) + ' ' + units[u];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
@ -24,7 +24,17 @@ const routes = [
|
|||||||
meta: {
|
meta: {
|
||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/status',
|
||||||
|
name: 'status',
|
||||||
|
component: function () {
|
||||||
|
return import(/* webpackChunkName: "Status" */ '../views/Status.vue')
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const router = new VueRouter({
|
const router = new VueRouter({
|
||||||
|
@ -11,7 +11,11 @@ const ApiService = {
|
|||||||
return Vue.axios.get(resource)
|
return Vue.axios.get(resource)
|
||||||
.then(response => response.data)
|
.then(response => response.data)
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
|
if(typeof error.response !== 'undefined') {
|
||||||
|
throw new Error(`${error.response.status} - ${error.response.statusText}: ${error.response.data}`)
|
||||||
|
} else {
|
||||||
throw new Error(`ApiService: ${error}`)
|
throw new Error(`ApiService: ${error}`)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import Vuex from 'vuex'
|
|||||||
import auth from "./modules/auth";
|
import auth from "./modules/auth";
|
||||||
import client from "./modules/client";
|
import client from "./modules/client";
|
||||||
import server from "./modules/server";
|
import server from "./modules/server";
|
||||||
|
import status from "./modules/status";
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
@ -14,6 +15,7 @@ export default new Vuex.Store({
|
|||||||
modules: {
|
modules: {
|
||||||
auth,
|
auth,
|
||||||
client,
|
client,
|
||||||
server
|
server,
|
||||||
|
status,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
97
ui/src/store/modules/status.js
Normal file
97
ui/src/store/modules/status.js
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import ApiService from "../../services/api.service";
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
error: null,
|
||||||
|
enabled: false,
|
||||||
|
interfaceStatus: null,
|
||||||
|
clientStatus: [],
|
||||||
|
version: '_ci_build_not_run_properly_',
|
||||||
|
}
|
||||||
|
|
||||||
|
const getters = {
|
||||||
|
error(state) {
|
||||||
|
return state.error;
|
||||||
|
},
|
||||||
|
|
||||||
|
enabled(state) {
|
||||||
|
return state.enabled;
|
||||||
|
},
|
||||||
|
|
||||||
|
interfaceStatus(state) {
|
||||||
|
return state.interfaceStatus;
|
||||||
|
},
|
||||||
|
|
||||||
|
clientStatus(state) {
|
||||||
|
return state.clientStatus;
|
||||||
|
},
|
||||||
|
|
||||||
|
version(state) {
|
||||||
|
return state.version;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
error({ commit }, error){
|
||||||
|
commit('error', error)
|
||||||
|
},
|
||||||
|
|
||||||
|
read({ commit }){
|
||||||
|
ApiService.get("/status/interface")
|
||||||
|
.then(resp => {
|
||||||
|
commit('interfaceStatus', resp)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
commit('interfaceStatus', null);
|
||||||
|
commit('error', err)
|
||||||
|
});
|
||||||
|
ApiService.get("/status/clients")
|
||||||
|
.then(resp => {
|
||||||
|
commit('clientStatus', resp)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
commit('clientStatus', []);
|
||||||
|
commit('error', err)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
isEnabled({ commit }){
|
||||||
|
ApiService.get("/status/enabled")
|
||||||
|
.then(resp => {
|
||||||
|
commit('enabled', resp)
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
commit('enabled', false);
|
||||||
|
commit('error', err.response.data)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
error(state, error) {
|
||||||
|
state.error = error;
|
||||||
|
},
|
||||||
|
|
||||||
|
enabled(state, enabled) {
|
||||||
|
state.enabled = enabled;
|
||||||
|
},
|
||||||
|
|
||||||
|
interfaceStatus(state, interfaceStatus){
|
||||||
|
state.interfaceStatus = interfaceStatus
|
||||||
|
},
|
||||||
|
|
||||||
|
clientStatus(state, clientStatus){
|
||||||
|
state.clientStatus = clientStatus
|
||||||
|
},
|
||||||
|
|
||||||
|
version(state, version){
|
||||||
|
state.version = version
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations
|
||||||
|
}
|
16
ui/src/views/Status.vue
Normal file
16
ui/src/views/Status.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<v-content>
|
||||||
|
<Status/>
|
||||||
|
</v-content>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Status from '../components/Status'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'status',
|
||||||
|
components: {
|
||||||
|
Status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
Loading…
Reference in New Issue
Block a user