From 0aad10cb63df40ef1ded5baac76ac7503ca52399 Mon Sep 17 00:00:00 2001
From: vx3r <vx3r@127-0-0-1.fr>
Date: Wed, 19 Feb 2020 16:19:16 +0900
Subject: [PATCH] refactor backend and frontend, migrate structs, organize
 front view, add mtu

---
 .gitlab-ci.yml                     |  22 +
 README.md                          |   1 +
 core/client.go                     |   7 +-
 core/migrate.go                    | 189 +++++++++
 core/server.go                     |  24 +-
 main.go                            |  17 +
 model/client.go                    |  17 +-
 model/server.go                    |  31 +-
 template/template.go               |  81 ++--
 ui/package-lock.json               |  33 +-
 ui/package.json                    |   1 -
 ui/src/components/Clients.vue      | 449 ++++++++++++++++++++
 ui/src/components/Notification.vue |  25 ++
 ui/src/components/Server.vue       | 230 +++++++++++
 ui/src/main.js                     |   1 -
 ui/src/plugins/axios.js            |  13 -
 ui/src/plugins/moment.js           |   2 +-
 ui/src/services/ApiService.js      |  40 ++
 ui/src/views/Home.vue              | 637 +----------------------------
 19 files changed, 1085 insertions(+), 735 deletions(-)
 create mode 100644 core/migrate.go
 create mode 100644 ui/src/components/Clients.vue
 create mode 100644 ui/src/components/Notification.vue
 create mode 100644 ui/src/components/Server.vue
 delete mode 100644 ui/src/plugins/axios.js
 create mode 100644 ui/src/services/ApiService.js

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index a17c0c5..4865708 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,7 +1,29 @@
 stages:
+  - build artifacts
   - build docker image
   - push docker hub
 
+build-back:
+  stage: build artifacts
+  image: golang:latest
+  script:
+    - GOOS=linux GOARCH=amd64 go build -o ${CI_PROJECT_NAME}-linux-amd64
+  artifacts:
+    paths:
+      - ${CI_PROJECT_NAME}-linux-amd64
+
+build-front:
+  stage: build artifacts
+  image: node:10-alpine
+  script:
+    - cd ./ui
+    - npm install
+    - npm run build
+    - cd ..
+  artifacts:
+    paths:
+      - ui/dist
+
 build:
   stage: build docker image
   image: docker:latest
diff --git a/README.md b/README.md
index af50f9c..0123076 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@ The goal is to run Wg Gen Web in a container and WireGuard on host system.
  * Self-hosted and web based
  * Automatically select IP from the netowrk pool assigned to client
  * QR-Code for convenient mobile client configuration
+ * Sent email to client with QR-code and client config
  * Enable / Disable client
  * Generation of `wg0.conf` after any modification
  * Dockerized
diff --git a/core/client.go b/core/client.go
index e2839fc..2a6ddec 100644
--- a/core/client.go
+++ b/core/client.go
@@ -16,7 +16,6 @@ import (
 	"path/filepath"
 	"sort"
 	"strconv"
-	"strings"
 	"time"
 )
 
@@ -49,8 +48,8 @@ func CreateClient(client *model.Client) (*model.Client, error) {
 	}
 
 	ips := make([]string, 0)
-	for _, network := range strings.Split(client.Address, ",") {
-		ip, err := util.GetAvailableIp(strings.TrimSpace(network), reserverIps)
+	for _, network := range client.Address {
+		ip, err := util.GetAvailableIp(network, reserverIps)
 		if err != nil {
 			return nil, err
 		}
@@ -61,7 +60,7 @@ func CreateClient(client *model.Client) (*model.Client, error) {
 		}
 		ips = append(ips, ip)
 	}
-	client.Address = strings.Join(ips, ",")
+	client.Address = ips
 	client.Created = time.Now().UTC()
 	client.Updated = client.Created
 
diff --git a/core/migrate.go b/core/migrate.go
new file mode 100644
index 0000000..e8e1137
--- /dev/null
+++ b/core/migrate.go
@@ -0,0 +1,189 @@
+package core
+
+import (
+	"encoding/json"
+	uuid "github.com/satori/go.uuid"
+	log "github.com/sirupsen/logrus"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/storage"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+)
+
+// Migrate all changes, current struct fields change
+func Migrate() error {
+	clients, err := readClients()
+	if err != nil {
+		return err
+	}
+
+	s, err := deserialize("server.json")
+	if err != nil {
+		return err
+	}
+
+	for _, client := range clients {
+		switch v := client["allowedIPs"].(type) {
+		case []interface{}:
+			log.Infof("client %s has been already migrated", client["id"])
+			continue
+		default:
+			log.Infof("unexpected type %T, mus be migrated", v)
+		}
+
+		c := &model.Client{}
+		c.Id = client["id"].(string)
+		c.Name = client["name"].(string)
+		c.Email = client["email"].(string)
+		c.Enable = client["enable"].(bool)
+		c.AllowedIPs = make([]string, 0)
+		for _, address := range strings.Split(client["allowedIPs"].(string), ",") {
+			if util.IsValidCidr(strings.TrimSpace(address)) {
+				c.AllowedIPs = append(c.AllowedIPs, strings.TrimSpace(address))
+			}
+		}
+		c.Address = make([]string, 0)
+		for _, address := range strings.Split(client["address"].(string), ",") {
+			if util.IsValidCidr(strings.TrimSpace(address)) {
+				c.Address = append(c.Address, strings.TrimSpace(address))
+			}
+		}
+		c.PrivateKey = client["privateKey"].(string)
+		c.PublicKey = client["publicKey"].(string)
+		created, err := time.Parse(time.RFC3339, client["created"].(string))
+		if err != nil {
+			log.WithFields(log.Fields{
+				"err": err,
+			}).Errorf("failed to parse time")
+			continue
+		}
+		c.Created = created
+		updated, err := time.Parse(time.RFC3339, client["updated"].(string))
+		if err != nil {
+			log.WithFields(log.Fields{
+				"err": err,
+			}).Errorf("failed to parse time")
+			continue
+		}
+		c.Updated = updated
+
+		err = storage.Serialize(c.Id, c)
+		if err != nil {
+			log.WithFields(log.Fields{
+				"err": err,
+			}).Errorf("failed to Serialize client")
+		}
+	}
+
+	switch v := s["address"].(type) {
+	case []interface{}:
+		log.Info("server has been already migrated")
+		return nil
+	default:
+		log.Infof("unexpected type %T, mus be migrated", v)
+	}
+
+	server := &model.Server{}
+
+	server.Address = make([]string, 0)
+	for _, address := range strings.Split(s["address"].(string), ",") {
+		if util.IsValidCidr(strings.TrimSpace(address)) {
+			server.Address = append(server.Address, strings.TrimSpace(address))
+		}
+	}
+	server.ListenPort = int(s["listenPort"].(float64))
+	server.PrivateKey = s["privateKey"].(string)
+	server.PublicKey = s["publicKey"].(string)
+	server.PresharedKey = s["presharedKey"].(string)
+	server.Endpoint = s["endpoint"].(string)
+	server.PersistentKeepalive = int(s["persistentKeepalive"].(float64))
+	server.Dns = make([]string, 0)
+	for _, address := range strings.Split(s["dns"].(string), ",") {
+		if util.IsValidIp(strings.TrimSpace(address)) {
+			server.Dns = append(server.Dns, strings.TrimSpace(address))
+		}
+	}
+	if val, ok := s["preUp"]; ok {
+		server.PreUp = val.(string)
+	}
+	if val, ok := s["postUp"]; ok {
+		server.PostUp = val.(string)
+	}
+	if val, ok := s["preDown"]; ok {
+		server.PreDown = val.(string)
+	}
+	if val, ok := s["postDown"]; ok {
+		server.PostDown = val.(string)
+	}
+	created, err := time.Parse(time.RFC3339, s["created"].(string))
+	if err != nil {
+		log.WithFields(log.Fields{
+			"err": err,
+		}).Errorf("failed to parse time")
+	}
+	server.Created = created
+	updated, err := time.Parse(time.RFC3339, s["updated"].(string))
+	if err != nil {
+		log.WithFields(log.Fields{
+			"err": err,
+		}).Errorf("failed to parse time")
+	}
+	server.Updated = updated
+
+	err = storage.Serialize("server.json", server)
+	if err != nil {
+		log.WithFields(log.Fields{
+			"err": err,
+		}).Errorf("failed to Serialize server")
+	}
+
+	return nil
+}
+
+func readClients() ([]map[string]interface{}, error) {
+	clients := make([]map[string]interface{}, 0)
+
+	files, err := ioutil.ReadDir(filepath.Join(os.Getenv("WG_CONF_DIR")))
+	if err != nil {
+		return nil, err
+	}
+
+	for _, f := range files {
+		// clients file name is an uuid
+		_, err := uuid.FromString(f.Name())
+		if err == nil {
+			c, err := deserialize(f.Name())
+			if err != nil {
+				log.WithFields(log.Fields{
+					"err":  err,
+					"path": f.Name(),
+				}).Error("failed to deserialize client")
+			} else {
+				clients = append(clients, c)
+			}
+		}
+	}
+
+	return clients, nil
+}
+
+func deserialize(id string) (map[string]interface{}, error) {
+	path := filepath.Join(os.Getenv("WG_CONF_DIR"), id)
+
+	data, err := util.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	var d map[string]interface{}
+	err = json.Unmarshal(data, &d)
+	if err != nil {
+		return nil, err
+	}
+
+	return d, nil
+}
diff --git a/core/server.go b/core/server.go
index a4535d7..6a94f33 100644
--- a/core/server.go
+++ b/core/server.go
@@ -10,7 +10,6 @@ import (
 	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 	"os"
 	"path/filepath"
-	"strings"
 	"time"
 )
 
@@ -32,12 +31,19 @@ func ReadServer() (*model.Server, error) {
 		}
 		server.PresharedKey = presharedKey.String()
 
-		server.Name = "Created with default values"
 		server.Endpoint = "wireguard.example.com:123"
 		server.ListenPort = 51820
-		server.Address = "fd9f:6666::10:6:6:1/112, 10.6.6.1/24"
-		server.Dns = "fd9f::10:0:0:2, 10.0.0.2"
+
+		server.Address = make([]string, 0)
+		server.Address = append(server.Address, "fd9f:6666::10:6:6:1/64")
+		server.Address = append(server.Address, "10.6.6.1/24")
+
+		server.Dns = make([]string, 0)
+		server.Dns = append(server.Dns, "fd9f::10:0:0:2")
+		server.Dns = append(server.Dns, "10.0.0.2")
+
 		server.PersistentKeepalive = 16
+		server.Mtu = 0
 		server.PreUp = "echo WireGuard PreUp"
 		server.PostUp = "echo WireGuard PostUp"
 		server.PreDown = "echo WireGuard PreDown"
@@ -131,12 +137,12 @@ func GetAllReservedIps() ([]string, error) {
 	reserverIps := make([]string, 0)
 
 	for _, client := range clients {
-		for _, cidr := range strings.Split(client.Address, ",") {
-			ip, err := util.GetIpFromCidr(strings.TrimSpace(cidr))
+		for _, cidr := range client.Address {
+			ip, err := util.GetIpFromCidr(cidr)
 			if err != nil {
 				log.WithFields(log.Fields{
 					"err":  err,
-					"cidr": err,
+					"cidr": cidr,
 				}).Error("failed to ip from cidr")
 			} else {
 				reserverIps = append(reserverIps, ip)
@@ -144,8 +150,8 @@ func GetAllReservedIps() ([]string, error) {
 		}
 	}
 
-	for _, cidr := range strings.Split(server.Address, ",") {
-		ip, err := util.GetIpFromCidr(strings.TrimSpace(cidr))
+	for _, cidr := range server.Address {
+		ip, err := util.GetIpFromCidr(cidr)
 		if err != nil {
 			log.WithFields(log.Fields{
 				"err":  err,
diff --git a/main.go b/main.go
index a5cdd96..737256a 100644
--- a/main.go
+++ b/main.go
@@ -9,6 +9,7 @@ import (
 	"github.com/joho/godotenv"
 	log "github.com/sirupsen/logrus"
 	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api"
+	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
 	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
 	"os"
 	"path/filepath"
@@ -40,6 +41,14 @@ func main() {
 		}
 	}
 
+	// check server.json or create it
+	_, err = core.ReadServer()
+	if err != nil {
+		log.WithFields(log.Fields{
+			"err": err,
+		}).Fatal("failed to ensure server.json exists")
+	}
+
 	if os.Getenv("GIN_MODE") == "debug" {
 		// set gin release debug
 		gin.SetMode(gin.DebugMode)
@@ -50,6 +59,14 @@ func main() {
 		gin.DisableConsoleColor()
 	}
 
+	// migrate
+	err = core.Migrate()
+	if err != nil {
+		log.WithFields(log.Fields{
+			"err": err,
+		}).Fatal("failed to migrate")
+	}
+
 	// creates a gin router with default middleware: logger and recovery (crash-free) middleware
 	app := gin.Default()
 
diff --git a/model/client.go b/model/client.go
index 3ff5dcb..eb0eb77 100644
--- a/model/client.go
+++ b/model/client.go
@@ -3,7 +3,6 @@ package model
 import (
 	"fmt"
 	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
-	"strings"
 	"time"
 )
 
@@ -13,8 +12,8 @@ type Client struct {
 	Name       string    `json:"name"`
 	Email      string    `json:"email"`
 	Enable     bool      `json:"enable"`
-	AllowedIPs string    `json:"allowedIPs"`
-	Address    string    `json:"address"`
+	AllowedIPs []string  `json:"allowedIPs"`
+	Address    []string  `json:"address"`
 	PrivateKey string    `json:"privateKey"`
 	PublicKey  string    `json:"publicKey"`
 	Created    time.Time `json:"created"`
@@ -41,22 +40,22 @@ func (a Client) IsValid() []error {
 		errs = append(errs, fmt.Errorf("email %s is invalid", a.Email))
 	}
 	// check if the allowedIPs empty
-	if a.AllowedIPs == "" {
+	if len(a.AllowedIPs) == 0 {
 		errs = append(errs, fmt.Errorf("allowedIPs field is required"))
 	}
 	// check if the allowedIPs are valid
-	for _, allowedIP := range strings.Split(a.AllowedIPs, ",") {
-		if !util.IsValidCidr(strings.TrimSpace(allowedIP)) {
+	for _, allowedIP := range a.AllowedIPs {
+		if !util.IsValidCidr(allowedIP) {
 			errs = append(errs, fmt.Errorf("allowedIP %s is invalid", allowedIP))
 		}
 	}
 	// check if the address empty
-	if a.Address == "" {
+	if len(a.Address) == 0 {
 		errs = append(errs, fmt.Errorf("address field is required"))
 	}
 	// check if the address are valid
-	for _, address := range strings.Split(a.Address, ",") {
-		if !util.IsValidCidr(strings.TrimSpace(address)) {
+	for _, address := range a.Address {
+		if !util.IsValidCidr(address) {
 			errs = append(errs, fmt.Errorf("address %s is invalid", address))
 		}
 	}
diff --git a/model/server.go b/model/server.go
index cb52ce0..1007a66 100644
--- a/model/server.go
+++ b/model/server.go
@@ -3,21 +3,20 @@ package model
 import (
 	"fmt"
 	"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
-	"strings"
 	"time"
 )
 
 // Server structure
 type Server struct {
-	Name                string    `json:"name"`
-	Address             string    `json:"address"`
+	Address             []string  `json:"address"`
 	ListenPort          int       `json:"listenPort"`
+	Mtu                 int       `json:"mtu"`
 	PrivateKey          string    `json:"privateKey"`
 	PublicKey           string    `json:"publicKey"`
 	PresharedKey        string    `json:"presharedKey"`
 	Endpoint            string    `json:"endpoint"`
 	PersistentKeepalive int       `json:"persistentKeepalive"`
-	Dns                 string    `json:"dns"`
+	Dns                 []string  `json:"dns"`
 	PreUp               string    `json:"preUp"`
 	PostUp              string    `json:"postUp"`
 	PreDown             string    `json:"preDown"`
@@ -29,21 +28,13 @@ type Server struct {
 func (a Server) IsValid() []error {
 	errs := make([]error, 0)
 
-	// check if the name empty
-	if a.Name == "" {
-		errs = append(errs, fmt.Errorf("name is required"))
-	}
-	// check the name field is between 3 to 40 chars
-	if len(a.Name) < 2 || len(a.Name) > 40 {
-		errs = append(errs, fmt.Errorf("name must be between 2-40 chars"))
-	}
 	// check if the address empty
-	if a.Address == "" {
+	if len(a.Address) == 0 {
 		errs = append(errs, fmt.Errorf("address is required"))
 	}
 	// check if the address are valid
-	for _, address := range strings.Split(a.Address, ",") {
-		if !util.IsValidCidr(strings.TrimSpace(address)) {
+	for _, address := range a.Address {
+		if !util.IsValidCidr(address) {
 			errs = append(errs, fmt.Errorf("address %s is invalid", address))
 		}
 	}
@@ -59,13 +50,13 @@ func (a Server) IsValid() []error {
 	if a.PersistentKeepalive < 0 {
 		errs = append(errs, fmt.Errorf("persistentKeepalive %d is invalid", a.PersistentKeepalive))
 	}
-	// check if the dns empty
-	if a.Dns == "" {
-		errs = append(errs, fmt.Errorf("dns is required"))
+	// check if the mtu is valid
+	if a.Mtu < 0 {
+		errs = append(errs, fmt.Errorf("MTU %d is invalid", a.PersistentKeepalive))
 	}
 	// check if the address are valid
-	for _, dns := range strings.Split(a.Dns, ",") {
-		if !util.IsValidIp(strings.TrimSpace(dns)) {
+	for _, dns := range a.Dns {
+		if !util.IsValidIp(dns) {
 			errs = append(errs, fmt.Errorf("dns %s is invalid", dns))
 		}
 	}
diff --git a/template/template.go b/template/template.go
index ea6a720..007a083 100644
--- a/template/template.go
+++ b/template/template.go
@@ -197,45 +197,54 @@ var (
 </html>
 `
 
-	clientTpl = `
-[Interface]
-Address = {{.Client.Address}}
-PrivateKey = {{.Client.PrivateKey}}
-DNS = {{.Server.Dns}}
+	clientTpl = `[Interface]
+Address = {{ StringsJoin .Client.Address ", " }}
+PrivateKey = {{ .Client.PrivateKey }}
+{{ if ne (len .Server.Dns) 0 -}}
+DNS = {{ StringsJoin .Server.Dns ", " }}
+{{- end }}
+{{ if ne .Server.Mtu 0 -}}
+MTU = {{.Server.Mtu}}
+{{- end}}
 [Peer]
-PublicKey = {{.Server.PublicKey}}
-PresharedKey = {{.Server.PresharedKey}}
-AllowedIPs = {{.Client.AllowedIPs}}
-Endpoint = {{.Server.Endpoint}}
-PersistentKeepalive = {{.Server.PersistentKeepalive}}`
+PublicKey = {{ .Server.PublicKey }}
+PresharedKey = {{ .Server.PresharedKey }}
+AllowedIPs = {{ StringsJoin .Client.AllowedIPs ", " }}
+Endpoint = {{ .Server.Endpoint }}
+{{ if ne .Server.PersistentKeepalive 0 -}}
+PersistentKeepalive = {{.Server.PersistentKeepalive}}
+{{- end}}
+`
 
-	wgTpl = `
-# {{.Server.Name}} / Updated: {{.Server.Updated}} / Created: {{.Server.Created}}
+	wgTpl = `# Updated: {{ .Server.Updated }} / Created: {{ .Server.Created }}
 [Interface]
-	{{range .ServerAdresses}}
-Address = {{.}}
-	{{end}}
-ListenPort = {{.Server.ListenPort}}
-PrivateKey = {{.Server.PrivateKey}}
-PreUp = {{.Server.PreUp}}
-PostUp = {{.Server.PostUp}}
-PreDown = {{.Server.PreDown}}
-PostDown = {{.Server.PostDown}}
-	{{$server := .Server}}
-	{{range .Clients}}
-		{{if .Enable}}
+{{- range .Server.Address }}
+Address = {{ . }}
+{{- end }}
+ListenPort = {{ .Server.ListenPort }}
+PrivateKey = {{ .Server.PrivateKey }}
+{{ if ne .Server.Mtu 0 -}}
+MTU = {{.Server.Mtu}}
+{{- end}}
+PreUp = {{ .Server.PreUp }}
+PostUp = {{ .Server.PostUp }}
+PreDown = {{ .Server.PreDown }}
+PostDown = {{ .Server.PostDown }}
+{{ $server := .Server }}
+{{- range .Clients }}
+{{ if .Enable -}}
 # {{.Name}} / {{.Email}} / Updated: {{.Updated}} / Created: {{.Created}}
 [Peer]
-PublicKey = {{.PublicKey}}
-PresharedKey = {{$server.PresharedKey}}
-AllowedIPs = {{.Address}}
-		{{end}}
-	{{end}}`
+PublicKey = {{ .PublicKey }}
+PresharedKey = {{ $server.PresharedKey }}
+AllowedIPs = {{ StringsJoin .Address ", " }}
+{{- end }}
+{{ end }}`
 )
 
 // DumpClientWg dump client wg config with go template
 func DumpClientWg(client *model.Client, server *model.Server) ([]byte, error) {
-	t, err := template.New("client").Parse(clientTpl)
+	t, err := template.New("client").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(clientTpl)
 	if err != nil {
 		return nil, err
 	}
@@ -251,19 +260,17 @@ func DumpClientWg(client *model.Client, server *model.Server) ([]byte, error) {
 
 // DumpServerWg dump server wg config with go template, write it to file and return bytes
 func DumpServerWg(clients []*model.Client, server *model.Server) ([]byte, error) {
-	t, err := template.New("server").Parse(wgTpl)
+	t, err := template.New("server").Funcs(template.FuncMap{"StringsJoin": strings.Join}).Parse(wgTpl)
 	if err != nil {
 		return nil, err
 	}
 
 	configDataWg, err := dump(t, struct {
-		Clients        []*model.Client
-		Server         *model.Server
-		ServerAdresses []string
+		Clients []*model.Client
+		Server  *model.Server
 	}{
-		ServerAdresses: strings.Split(server.Address, ","),
-		Clients:        clients,
-		Server:         server,
+		Clients: clients,
+		Server:  server,
 	})
 	if err != nil {
 		return nil, err
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 6a3bf90..5a594d8 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -3561,12 +3561,14 @@
         "balanced-match": {
           "version": "1.0.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "brace-expansion": {
           "version": "1.1.11",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "balanced-match": "^1.0.0",
             "concat-map": "0.0.1"
@@ -3581,17 +3583,20 @@
         "code-point-at": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "concat-map": {
           "version": "0.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "console-control-strings": {
           "version": "1.1.0",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "core-util-is": {
           "version": "1.0.2",
@@ -3708,7 +3713,8 @@
         "inherits": {
           "version": "2.0.4",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "ini": {
           "version": "1.3.5",
@@ -3720,6 +3726,7 @@
           "version": "1.0.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "number-is-nan": "^1.0.0"
           }
@@ -3734,6 +3741,7 @@
           "version": "3.0.4",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "brace-expansion": "^1.1.7"
           }
@@ -3741,12 +3749,14 @@
         "minimist": {
           "version": "0.0.8",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "minipass": {
           "version": "2.9.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "safe-buffer": "^5.1.2",
             "yallist": "^3.0.0"
@@ -3765,6 +3775,7 @@
           "version": "0.5.1",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "minimist": "0.0.8"
           }
@@ -3854,7 +3865,8 @@
         "number-is-nan": {
           "version": "1.0.1",
           "bundled": true,
-          "dev": true
+          "dev": true,
+          "optional": true
         },
         "object-assign": {
           "version": "4.1.1",
@@ -3866,6 +3878,7 @@
           "version": "1.4.0",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "wrappy": "1"
           }
@@ -3987,6 +4000,7 @@
           "version": "1.0.2",
           "bundled": true,
           "dev": true,
+          "optional": true,
           "requires": {
             "code-point-at": "^1.0.0",
             "is-fullwidth-code-point": "^1.0.0",
@@ -8920,11 +8934,6 @@
         "moment": "^2.19.2"
       }
     },
-    "vue-plugin-axios": {
-      "version": "1.0.14",
-      "resolved": "https://registry.npmjs.org/vue-plugin-axios/-/vue-plugin-axios-1.0.14.tgz",
-      "integrity": "sha512-hpFdi17XsSdYQnrlHrml4BnmH9ceJSkpCoMGbPCz5vfDoY7okWuzKgeKK44NgRlZzlUCrq7ug3fGLZ2nv/qNNA=="
-    },
     "vue-router": {
       "version": "3.1.5",
       "resolved": "https://registry.npm.taobao.org/vue-router/download/vue-router-3.1.5.tgz",
diff --git a/ui/package.json b/ui/package.json
index 82eb328..64aff46 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -12,7 +12,6 @@
     "moment": "^2.24.0",
     "vue": "^2.6.10",
     "vue-moment": "^4.1.0",
-    "vue-plugin-axios": "^1.0.14",
     "vue-router": "^3.1.3",
     "vuetify": "^2.1.0"
   },
diff --git a/ui/src/components/Clients.vue b/ui/src/components/Clients.vue
new file mode 100644
index 0000000..71f71f9
--- /dev/null
+++ b/ui/src/components/Clients.vue
@@ -0,0 +1,449 @@
+<template>
+    <v-container>
+        <v-row>
+            <v-col cols="12">
+                <v-card dark>
+                    <v-list-item>
+                        <v-list-item-content>
+                            <v-list-item-title class="headline">Clients</v-list-item-title>
+                        </v-list-item-content>
+                        <v-btn
+                                color="success"
+                                @click="startAddClient"
+                        >
+                            Add new client
+                            <v-icon right dark>mdi-account-multiple-plus-outline</v-icon>
+                        </v-btn>
+                    </v-list-item>
+                    <v-row>
+                        <v-col
+                                v-for="(client, i) in clients"
+                                :key="i"
+                                cols="6"
+                        >
+                            <v-card
+                                    :color="client.enable ? '#1F7087' : 'warning'"
+                                    class="mx-auto"
+                                    raised
+                                    shaped
+                            >
+                                <v-list-item>
+                                    <v-list-item-content>
+                                        <v-list-item-title class="headline">{{ client.name }}</v-list-item-title>
+                                        <v-list-item-subtitle>{{ client.email }}</v-list-item-subtitle>
+                                        <v-list-item-subtitle>Created: {{ client.created | formatDate }}</v-list-item-subtitle>
+                                        <v-list-item-subtitle>Updated: {{ client.updated | formatDate }}</v-list-item-subtitle>
+                                    </v-list-item-content>
+
+                                    <v-list-item-avatar
+                                            tile
+                                            size="150"
+                                    >
+                                        <v-img :src="`${apiBaseUrl}/client/${client.id}/config?qrcode=true`"/>
+                                    </v-list-item-avatar>
+                                </v-list-item>
+
+                                <v-card-text class="text--primary">
+                                    <v-chip
+                                            v-for="(ip, i) in client.address"
+                                            :key="i"
+                                            color="indigo"
+                                            text-color="white"
+                                    >
+                                        <v-icon left>mdi-ip-network</v-icon>
+                                        {{ ip }}
+                                    </v-chip>
+                                </v-card-text>
+                                <v-card-actions>
+                                    <v-btn
+                                            text
+                                            :href="`${apiBaseUrl}/client/${client.id}/config?qrcode=false`"
+                                    >
+                                        Download
+                                        <v-icon right dark>mdi-cloud-download-outline</v-icon>
+                                    </v-btn>
+                                    <v-btn
+                                            text
+                                            @click.stop="startUpdateClient(client)"
+                                    >
+                                        Edit
+                                        <v-icon right dark>mdi-square-edit-outline</v-icon>
+                                    </v-btn>
+                                    <v-btn
+                                            text
+                                            @click="deleteClient(client)"
+                                    >
+                                        Delete
+                                        <v-icon right dark>mdi-trash-can-outline</v-icon>
+                                    </v-btn>
+                                    <v-btn
+                                            text
+                                            @click="sendEmailClient(client.id)"
+                                    >
+                                        Send email
+                                        <v-icon right dark>mdi-email-send-outline</v-icon>
+                                    </v-btn>
+                                    <v-spacer/>
+                                    <v-tooltip right>
+                                        <template v-slot:activator="{ on }">
+                                            <v-switch
+                                                    dark
+                                                    v-on="on"
+                                                    color="success"
+                                                    v-model="client.enable"
+                                                    v-on:change="updateClient(client)"
+                                            />
+                                        </template>
+                                        <span> {{client.enable ? 'Disable' : 'Enable'}} this client</span>
+                                    </v-tooltip>
+
+                                </v-card-actions>
+                            </v-card>
+                        </v-col>
+                    </v-row>
+                </v-card>
+            </v-col>
+        </v-row>
+        <v-dialog
+                v-if="client"
+                v-model="dialogAddClient"
+                max-width="550"
+        >
+            <v-card>
+                <v-card-title class="headline">Add new client</v-card-title>
+                <v-card-text>
+                    <v-row>
+                        <v-col
+                                cols="12"
+                        >
+                            <v-form
+                                    ref="form"
+                                    v-model="valid"
+                            >
+                                <v-text-field
+                                        v-model="client.name"
+                                        label="Client friendly name"
+                                        :rules="[
+                          v => !!v || 'Client name is required',
+                        ]"
+                                        required
+                                />
+                                <v-text-field
+                                        v-model="client.email"
+                                        label="Client email"
+                                        :rules="[
+                        v => !!v || 'E-mail is required',
+                        v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
+                      ]"
+                                        required
+                                />
+                                <v-select
+                                        v-model="client.address"
+                                        :items="server.address"
+                                        label="Client IP will be chosen from these networks"
+                                        :rules="[
+                                v => !!v || 'Network is required',
+                        ]"
+                                        multiple
+                                        chips
+                                        persistent-hint
+                                        required
+                                />
+                                <v-combobox
+                                        v-model="client.allowedIPs"
+                                        chips
+                                        hint="Write IPv4 or IPv6 CIDR and hit enter"
+                                        label="Allowed IPs"
+                                        multiple
+                                        dark
+                                >
+                                    <template v-slot:selection="{ attrs, item, select, selected }">
+                                        <v-chip
+                                                v-bind="attrs"
+                                                :input-value="selected"
+                                                close
+                                                @click="select"
+                                                @click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
+                                        >
+                                            <strong>{{ item }}</strong>&nbsp;
+                                        </v-chip>
+                                    </template>
+                                </v-combobox>
+
+                                <v-switch
+                                        v-model="client.enable"
+                                        color="red"
+                                        inset
+                                        :label="client.enable ? 'Enable client after creation': 'Disable client after creation'"
+                                />
+                            </v-form>
+                        </v-col>
+                    </v-row>
+                </v-card-text>
+                <v-card-actions>
+                    <v-spacer/>
+                    <v-btn
+                            :disabled="!valid"
+                            color="success"
+                            @click="addClient(client)"
+                    >
+                        Submit
+                        <v-icon right dark>mdi-check-outline</v-icon>
+                    </v-btn>
+                    <v-btn
+                            color="primary"
+                            @click="dialogAddClient = false"
+                    >
+                        Cancel
+                        <v-icon right dark>mdi-close-circle-outline</v-icon>
+                    </v-btn>
+                </v-card-actions>
+            </v-card>
+        </v-dialog>
+        <v-dialog
+                v-if="client"
+                v-model="dialogEditClient"
+                max-width="550"
+        >
+            <v-card>
+                <v-card-title class="headline">Edit client</v-card-title>
+                <v-card-text>
+                    <v-row>
+                        <v-col
+                                cols="12"
+                        >
+                            <v-form
+                                    ref="form"
+                                    v-model="valid"
+                            >
+                                <v-text-field
+                                        v-model="client.name"
+                                        label="Friendly name"
+                                        :rules="[
+                          v => !!v || 'Client name is required',
+                        ]"
+                                        required
+                                />
+                                <v-text-field
+                                        v-model="client.email"
+                                        label="Email"
+                                        :rules="[
+                        v => !!v || 'Email is required',
+                        v => /.+@.+\..+/.test(v) || 'Email must be valid',
+                        ]"
+                                        required
+                                />
+                                <v-combobox
+                                        v-model="client.address"
+                                        chips
+                                        hint="Write IPv4 or IPv6 CIDR and hit enter"
+                                        label="Addresses"
+                                        multiple
+                                        dark
+                                >
+                                    <template v-slot:selection="{ attrs, item, select, selected }">
+                                        <v-chip
+                                                v-bind="attrs"
+                                                :input-value="selected"
+                                                close
+                                                @click="select"
+                                                @click:close="client.address.splice(client.address.indexOf(item), 1)"
+                                        >
+                                            <strong>{{ item }}</strong>&nbsp;
+                                        </v-chip>
+                                    </template>
+                                </v-combobox>
+                                <v-combobox
+                                        v-model="client.allowedIPs"
+                                        chips
+                                        hint="Write IPv4 or IPv6 CIDR and hit enter"
+                                        label="Allowed IPs"
+                                        multiple
+                                        dark
+                                >
+                                    <template v-slot:selection="{ attrs, item, select, selected }">
+                                        <v-chip
+                                                v-bind="attrs"
+                                                :input-value="selected"
+                                                close
+                                                @click="select"
+                                                @click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
+                                        >
+                                            <strong>{{ item }}</strong>&nbsp;
+                                        </v-chip>
+                                    </template>
+                                </v-combobox>
+                            </v-form>
+                        </v-col>
+                    </v-row>
+                </v-card-text>
+                <v-card-actions>
+                    <v-spacer/>
+                    <v-btn
+                            :disabled="!valid"
+                            color="success"
+                            @click="updateClient(client)"
+                    >
+                        Submit
+                        <v-icon right dark>mdi-check-outline</v-icon>
+                    </v-btn>
+                    <v-btn
+                            color="primary"
+                            @click="dialogEditClient = false"
+                    >
+                        Cancel
+                        <v-icon right dark>mdi-close-circle-outline</v-icon>
+                    </v-btn>
+                </v-card-actions>
+            </v-card>
+        </v-dialog>
+        <Notification v-bind:notification="notification"/>
+    </v-container>
+</template>
+<script>
+  import {ApiService, API_BASE_URL} from '../services/ApiService'
+  import Notification from '../components/Notification'
+
+  export default {
+    name: 'Clients',
+
+    components: {
+      Notification
+    },
+
+    data: () => ({
+      api: null,
+      apiBaseUrl: API_BASE_URL,
+      clients: [],
+      notification: {
+        show: false,
+        color: '',
+        text: '',
+      },
+      dialogAddClient: false,
+      dialogEditClient: false,
+      client: null,
+      server: null,
+      valid: false,
+    }),
+
+    mounted () {
+      this.api = new ApiService();
+      this.getClients();
+      this.getServer()
+    },
+
+    methods: {
+      getClients() {
+        this.api.get('/client').then((res) => {
+          this.clients = res
+        }).catch((e) => {
+          this.notify('error', e.response.status + ' ' + e.response.statusText);
+        });
+      },
+
+      getServer() {
+        this.api.get('/server').then((res) => {
+          this.server = res;
+        }).catch((e) => {
+          this.notify('error', e.response.status + ' ' + e.response.statusText);
+        });
+      },
+
+      startAddClient() {
+        this.dialogAddClient = true;
+        this.client = {
+          name: "",
+          email: "",
+          enable: true,
+          allowedIPs: ["0.0.0.0/0", "::/0"],
+          address: this.server.address,
+        }
+      },
+      addClient(client) {
+        if (client.allowedIPs.length < 1) {
+          this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
+          return;
+        }
+        for (let i = 0; i < client.allowedIPs.length; i++){
+          if (this.$isCidr(client.allowedIPs[i]) === 0) {
+            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
+            return
+          }
+        }
+        this.dialogAddClient = false;
+
+        this.api.post('/client', client).then((res) => {
+          this.notify('success', `Client ${res.name} successfully added`);
+          this.getClients()
+        }).catch((e) => {
+          this.notify('error', e.response.status + ' ' + e.response.statusText);
+        });
+      },
+
+      deleteClient(client) {
+        if(confirm(`Do you really want to delete ${client.name} ?`)){
+          this.api.delete(`/client/${client.id}`).then((res) => {
+            this.notify('success', "Client successfully deleted");
+            this.getClients()
+          }).catch((e) => {
+            this.notify('error', e.response.status + ' ' + e.response.statusText);
+          });
+        }
+      },
+
+      sendEmailClient(id) {
+        this.api.get(`/client/${id}/email`).then((res) => {
+          this.notify('success', "Email successfully sent");
+          this.getClients()
+        }).catch((e) => {
+          this.notify('error', e.response.status + ' ' + e.response.statusText);
+        });
+      },
+
+      startUpdateClient(client) {
+        this.client = client;
+        this.dialogEditClient = true;
+      },
+      updateClient(client) {
+        // check allowed IPs
+        if (client.allowedIPs.length < 1) {
+          this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
+          return;
+        }
+        for (let i = 0; i < client.allowedIPs.length; i++){
+          if (this.$isCidr(client.allowedIPs[i]) === 0) {
+            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
+            return
+          }
+        }
+        // check address
+        if (client.address.length < 1) {
+          this.notify('error', 'Please provide at least one valid CIDR address for client');
+          return;
+        }
+        for (let i = 0; i < client.address.length; i++){
+          if (this.$isCidr(client.address[i]) === 0) {
+            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
+            return
+          }
+        }
+        // all good, submit
+        this.dialogEditClient = false;
+
+        this.api.patch(`/client/${client.id}`, client).then((res) => {
+          this.notify('success', `Client ${res.name} successfully updated`);
+          this.getClients()
+        }).catch((e) => {
+          this.notify('error', e.response.status + ' ' + e.response.statusText);
+        });
+      },
+
+      notify(color, msg) {
+        this.notification.show = true;
+        this.notification.color = color;
+        this.notification.text = msg;
+      }
+    }
+  };
+</script>
diff --git a/ui/src/components/Notification.vue b/ui/src/components/Notification.vue
new file mode 100644
index 0000000..a898aa6
--- /dev/null
+++ b/ui/src/components/Notification.vue
@@ -0,0 +1,25 @@
+<template>
+    <v-snackbar
+            v-model="notification.show"
+            :right="true"
+            :top="true"
+            :color="notification.color"
+    >
+        {{ notification.text }}
+        <v-btn
+                dark
+                text
+                @click="notification.show = false"
+        >
+            Close
+        </v-btn>
+    </v-snackbar>
+</template>
+<script>
+  export default {
+    name: 'Notification',
+    props: ['notification'],
+    data: () => ({
+    }),
+  };
+</script>
diff --git a/ui/src/components/Server.vue b/ui/src/components/Server.vue
new file mode 100644
index 0000000..01e9106
--- /dev/null
+++ b/ui/src/components/Server.vue
@@ -0,0 +1,230 @@
+<template>
+    <v-container v-if="server">
+        <v-row>
+            <v-col cols="6">
+                <v-card dark>
+                    <v-list-item>
+                        <v-list-item-content>
+                            <v-list-item-title class="headline">Server's interface configuration</v-list-item-title>
+                        </v-list-item-content>
+                    </v-list-item>
+                    <div class="d-flex flex-no-wrap justify-space-between">
+                        <v-col cols="12">
+                            <v-text-field
+                                    v-model="server.publicKey"
+                                    label="Public key"
+                                    disabled
+                            />
+                            <v-text-field
+                                    v-model="server.presharedKey"
+                                    label="Preshared key"
+                                    disabled
+                            />
+                            <v-text-field
+                                    v-model="server.listenPort"
+                                    type="number"
+                                    :rules="[
+                          v => !!v || 'Listen port is required',
+                        ]"
+                                    label="Listen port"
+                                    required
+                            />
+                            <v-combobox
+                                    v-model="server.address"
+                                    chips
+                                    hint="Write IPv4 or IPv6 CIDR and hit enter"
+                                    label="Server interface addresses"
+                                    multiple
+                                    dark
+                            >
+                                <template v-slot:selection="{ attrs, item, select, selected }">
+                                    <v-chip
+                                            v-bind="attrs"
+                                            :input-value="selected"
+                                            close
+                                            @click="select"
+                                            @click:close="server.address.splice(server.address.indexOf(item), 1)"
+                                    >
+                                        <strong>{{ item }}</strong>&nbsp;
+                                    </v-chip>
+                                </template>
+                            </v-combobox>
+                        </v-col>
+                    </div>
+                </v-card>
+            </v-col>
+            <v-col cols="6">
+                <v-card dark>
+                    <v-list-item>
+                        <v-list-item-content>
+                            <v-list-item-title class="headline">Client's global configuration</v-list-item-title>
+                        </v-list-item-content>
+                    </v-list-item>
+                    <div class="d-flex flex-no-wrap justify-space-between">
+                        <v-col cols="12">
+                            <v-text-field
+                                    v-model="server.endpoint"
+                                    label="Public endpoint for clients to connect to"
+                                    :rules="[
+                          v => !!v || 'Public endpoint for clients to connect to is required',
+                        ]"
+                                    required
+                            />
+                            <v-combobox
+                                    v-model="server.dns"
+                                    chips
+                                    hint="Write IPv4 or IPv6 address and hit enter"
+                                    label="DNS servers for clients"
+                                    multiple
+                                    dark
+                            >
+                                <template v-slot:selection="{ attrs, item, select, selected }">
+                                    <v-chip
+                                            v-bind="attrs"
+                                            :input-value="selected"
+                                            close
+                                            @click="select"
+                                            @click:close="server.dns.splice(server.dns.indexOf(item), 1)"
+                                    >
+                                        <strong>{{ item }}</strong>&nbsp;
+                                    </v-chip>
+                                </template>
+                            </v-combobox>
+                            <v-text-field
+                                    type="number"
+                                    v-model="server.mtu"
+                                    label="Define global MTU"
+                                    hint="Leave at 0 and let wg-quick take care of MTU"
+                            />
+                            <v-text-field
+                                    type="number"
+                                    v-model="server.persistentKeepalive"
+                                    label="Persistent keepalive"
+                                    hint="Leave at 0 if you dont want to specify persistent keepalive"
+                            />
+                        </v-col>
+                    </div>
+                </v-card>
+            </v-col>
+        </v-row>
+        <v-row>
+            <v-col cols="12">
+                <v-card dark>
+                    <v-list-item>
+                        <v-list-item-content>
+                            <v-list-item-title class="headline">Interface configuration hooks</v-list-item-title>
+                        </v-list-item-content>
+                    </v-list-item>
+                    <div class="d-flex flex-no-wrap justify-space-between">
+                        <v-col cols="12">
+                            <v-text-field
+                                    v-model="server.preUp"
+                                    label="PreUp: script snippets which will be executed by bash before setting up the interface"
+                            />
+                            <v-text-field
+                                    v-model="server.postUp"
+                                    label="PostUp: script snippets which will be executed by bash after setting up the interface"
+                            />
+                            <v-text-field
+                                    v-model="server.preDown"
+                                    label="PreDown: script snippets which will be executed by bash before setting down the interface"
+                            />
+                            <v-text-field
+                                    v-model="server.postDown "
+                                    label="PostDown : script snippets which will be executed by bash after setting down the interface"
+                            />
+                        </v-col>
+                    </div>
+                </v-card>
+            </v-col>
+        </v-row>
+        <v-divider dark/>
+        <v-divider dark/>
+        <v-row justify="center">
+            <v-btn
+                    class="ma-2"
+                    color="warning"
+                    @click="updateServer"
+            >
+                Update server configuration
+                <v-icon right dark>mdi-update</v-icon>
+            </v-btn>
+        </v-row>
+        <Notification v-bind:notification="notification"/>
+    </v-container>
+</template>
+<script>
+  import {ApiService} from "../services/ApiService";
+  import Notification from '../components/Notification'
+
+  export default {
+    name: 'Server',
+
+    components: {
+      Notification
+    },
+
+    data: () => ({
+      api: null,
+      server: null,
+      notification: {
+        show: false,
+        color: '',
+        text: '',
+      },
+    }),
+
+    mounted () {
+      this.api = new ApiService();
+      this.getServer()
+    },
+
+    methods: {
+      getServer() {
+        this.api.get('/server').then((res) => {
+          this.server = res;
+        }).catch((e) => {
+          this.notify('error', e.response.status + ' ' + e.response.statusText);
+        });
+      },
+      updateServer () {
+        // convert int values
+        this.server.listenPort = parseInt(this.server.listenPort, 10);
+        this.server.persistentKeepalive = parseInt(this.server.persistentKeepalive, 10);
+        this.server.mtu = parseInt(this.server.mtu, 10);
+
+        // check server addresses
+        if (this.server.address.length < 1) {
+          this.notify('error', 'Please provide at least one valid CIDR address for server interface');
+          return;
+        }
+        for (let i = 0; i < this.server.address.length; i++){
+          if (this.$isCidr(this.server.address[i]) === 0) {
+            this.notify('error', `Invalid CIDR detected, please correct ${this.server.address[i]} before submitting`);
+            return
+          }
+        }
+
+        // check DNS correct
+        for (let i = 0; i < this.server.dns.length; i++){
+          if (this.$isCidr(this.server.dns[i] + '/32') === 0) {
+            this.notify('error', `Invalid IP detected, please correct ${this.server.dns[i]} before submitting`);
+            return
+          }
+        }
+
+        this.api.patch('/server', this.server).then((res) => {
+          this.notify('success', "Server successfully updated");
+          this.server = res;
+        }).catch((e) => {
+          this.notify('error', e.response.status + ' ' + e.response.statusText);
+        });
+      },
+      notify(color, msg) {
+        this.notification.show = true;
+        this.notification.color = color;
+        this.notification.text = msg;
+      }
+    }
+  };
+</script>
diff --git a/ui/src/main.js b/ui/src/main.js
index 6ba4187..bfb2c50 100644
--- a/ui/src/main.js
+++ b/ui/src/main.js
@@ -2,7 +2,6 @@ import Vue from 'vue'
 import App from './App.vue'
 import router from './router'
 import vuetify from './plugins/vuetify';
-import './plugins/axios';
 import './plugins/moment';
 import './plugins/cidr'
 
diff --git a/ui/src/plugins/axios.js b/ui/src/plugins/axios.js
deleted file mode 100644
index 11c1ce5..0000000
--- a/ui/src/plugins/axios.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import Vue from 'vue'
-import VueAxios from 'vue-plugin-axios'
-import axios from 'axios'
-
-export const myVar = 'This is my variable'
-
-// https://www.npmjs.com/package/vue-cli-plugin-vuetify
-Vue.use(VueAxios, {
-  axios,
-  config: {
-    baseURL: process.env.VUE_APP_API_BASE_URL || '/api/v1.0',
-  },
-});
diff --git a/ui/src/plugins/moment.js b/ui/src/plugins/moment.js
index b03fce7..5e8ebb0 100644
--- a/ui/src/plugins/moment.js
+++ b/ui/src/plugins/moment.js
@@ -2,7 +2,7 @@ import Vue from 'vue'
 import moment from 'moment';
 import VueMoment from 'vue-moment'
 
-moment.locale('es');
+moment.locale('en');
 
 Vue.use(VueMoment, {
   moment
diff --git a/ui/src/services/ApiService.js b/ui/src/services/ApiService.js
new file mode 100644
index 0000000..dc55572
--- /dev/null
+++ b/ui/src/services/ApiService.js
@@ -0,0 +1,40 @@
+import axios from 'axios'
+
+let baseUrl = "/api/v1.0";
+if (process.env.NODE_ENV === "development"){
+  baseUrl = process.env.VUE_APP_API_BASE_URL
+}
+
+export const API_BASE_URL = baseUrl;
+
+export class ApiService {
+  get(resource) {
+    return axios
+      .get(`${API_BASE_URL}${resource}`)
+      .then(response => response.data)
+  };
+
+  post(resource, data) {
+    return axios
+      .post(`${API_BASE_URL}${resource}`, data)
+      .then(response => response.data)
+  };
+
+  put(resource, data) {
+    return axios
+      .put(`${API_BASE_URL}${resource}`, data)
+      .then(response => response.data)
+  };
+
+  patch(resource, data) {
+    return axios
+      .patch(`${API_BASE_URL}${resource}`, data)
+      .then(response => response.data)
+  };
+
+  delete(resource) {
+    return axios
+      .delete(`${API_BASE_URL}${resource}`)
+      .then(response => response.data)
+  };
+}
diff --git a/ui/src/views/Home.vue b/ui/src/views/Home.vue
index 7a6d2db..6ac0b8a 100644
--- a/ui/src/views/Home.vue
+++ b/ui/src/views/Home.vue
@@ -1,638 +1,19 @@
 <template>
   <v-content>
-    <v-row v-if="server">
-      <v-col cols="12">
-        <v-card dark>
-          <v-list-item>
-            <v-list-item-content>
-              <v-list-item-title class="headline">Server configurations</v-list-item-title>
-            </v-list-item-content>
-          </v-list-item>
-          <div class="d-flex flex-no-wrap justify-space-between">
-            <v-col cols="6">
-              <v-text-field
-                      v-model="server.name"
-                      :rules="[
-                          v => !!v || 'Friendly name is required',
-                        ]"
-                      label="Friendly name"
-                      required
-              />
-              <v-text-field
-                      type="number"
-                      v-model="server.persistentKeepalive"
-                      label="Persistent keepalive"
-                      :rules="[
-                          v => !!v || 'Persistent keepalive is required',
-                        ]"
-                      required
-              />
-              <v-text-field
-                      v-model="server.endpoint"
-                      label="Public endpoint for clients to connect to"
-                      :rules="[
-                          v => !!v || 'Public endpoint for clients to connect to is required',
-                        ]"
-                      required
-              />
-              <v-combobox
-                      v-model="server.address"
-                      chips
-                      hint="Write IPv4 or IPv6 CIDR and hit enter"
-                      label="Server interface addresses"
-                      multiple
-                      dark
-              >
-                <template v-slot:selection="{ attrs, item, select, selected }">
-                  <v-chip
-                          v-bind="attrs"
-                          :input-value="selected"
-                          close
-                          @click="select"
-                          @click:close="server.address.splice(server.address.indexOf(item), 1)"
-                  >
-                    <strong>{{ item }}</strong>&nbsp;
-                  </v-chip>
-                </template>
-              </v-combobox>
-              <v-text-field
-                      v-model="server.preUp"
-                      label="PreUp: script snippets which will be executed by bash before setting up the interface"
-              />
-              <v-text-field
-                      v-model="server.postUp"
-                      label="PostUp: script snippets which will be executed by bash after setting up the interface"
-              />
-            </v-col>
-            <v-col cols="6">
-              <v-text-field
-                      v-model="server.publicKey"
-                      label="Public key"
-                      disabled
-              />
-              <v-text-field
-                      v-model="server.presharedKey"
-                      label="Preshared key"
-                      disabled
-              />
-              <v-text-field
-                      v-model="server.listenPort"
-                      type="number"
-                      :rules="[
-                          v => !!v || 'Listen port is required',
-                        ]"
-                      label="Listen port"
-                      required
-              />
-              <v-combobox
-                      v-model="server.dns"
-                      chips
-                      hint="Write IPv4 or IPv6 address and hit enter"
-                      label="DNS servers for clients"
-                      multiple
-                      dark
-              >
-                <template v-slot:selection="{ attrs, item, select, selected }">
-                  <v-chip
-                          v-bind="attrs"
-                          :input-value="selected"
-                          close
-                          @click="select"
-                          @click:close="server.dns.splice(server.dns.indexOf(item), 1)"
-                  >
-                    <strong>{{ item }}</strong>&nbsp;
-                  </v-chip>
-                </template>
-              </v-combobox>
-              <v-text-field
-                      v-model="server.preDown"
-                      label="PreDown: script snippets which will be executed by bash before setting down the interface"
-              />
-              <v-text-field
-                      v-model="server.postDown "
-                      label="PostDown : script snippets which will be executed by bash after setting down the interface"
-              />
-            </v-col>
-          </div>
-
-          <v-card-actions>
-            <v-spacer/>
-            <v-btn
-                    class="ma-2"
-                    color="warning"
-                    @click="updateServer"
-            >
-              Update server configuration
-              <v-icon right dark>mdi-update</v-icon>
-            </v-btn>
-          </v-card-actions>
-        </v-card>
-      </v-col>
-    </v-row>
-    <v-divider dark/>
-    <v-row>
-      <v-col cols="12">
-        <v-card dark>
-          <v-list-item>
-            <v-list-item-content>
-              <v-list-item-title class="headline">Clients</v-list-item-title>
-            </v-list-item-content>
-            <v-btn
-                    color="success"
-                    @click.stop="startAddClient"
-            >
-              Add new client
-              <v-icon right dark>mdi-account-multiple-plus-outline</v-icon>
-            </v-btn>
-          </v-list-item>
-          <v-row>
-            <v-col
-                    v-for="(client, i) in clients"
-                    :key="i"
-                    cols="6"
-            >
-              <v-card
-                      :color="client.enable ? '#1F7087' : 'warning'"
-                      class="mx-auto"
-                      raised
-                      shaped
-              >
-                <v-list-item>
-                  <v-list-item-content>
-                    <v-list-item-title class="headline">{{ client.name }}</v-list-item-title>
-                    <v-list-item-subtitle>{{ client.email }}</v-list-item-subtitle>
-                    <v-list-item-subtitle>Created: {{ client.created | formatDate }}</v-list-item-subtitle>
-                    <v-list-item-subtitle>Updated: {{ client.updated | formatDate }}</v-list-item-subtitle>
-                  </v-list-item-content>
-
-                  <v-list-item-avatar
-                          tile
-                          size="150"
-                  >
-                    <v-img :src="getUrlToConfig(client.id, true)"/>
-                  </v-list-item-avatar>
-                </v-list-item>
-
-                <v-card-text class="text--primary">
-                  <v-chip
-                          v-for="(ip, i) in client.address.split(',')"
-                          :key="i"
-                          color="indigo"
-                          text-color="white"
-                  >
-                    <v-icon left>mdi-ip-network</v-icon>
-                    {{ ip }}
-                  </v-chip>
-                </v-card-text>
-                <v-card-actions>
-                  <v-btn
-                          text
-                          :href="getUrlToConfig(client.id, false)"
-                  >
-                    Download
-                    <v-icon right dark>mdi-cloud-download-outline</v-icon>
-                  </v-btn>
-                  <v-btn
-                          text
-                          @click.stop="editClient(client.id)"
-                  >
-                    Edit
-                    <v-icon right dark>mdi-square-edit-outline</v-icon>
-                  </v-btn>
-                  <v-btn
-                          text
-                          @click="deleteClient(client.id)"
-                  >
-                    Delete
-                    <v-icon right dark>mdi-trash-can-outline</v-icon>
-                  </v-btn>
-                  <v-btn
-                          text
-                          @click="sendEmailClient(client.id)"
-                  >
-                    Send email
-                    <v-icon right dark>mdi-email-send-outline</v-icon>
-                  </v-btn>
-                  <v-spacer/>
-                  <v-tooltip right>
-                    <template v-slot:activator="{ on }">
-                      <v-switch
-                              dark
-                              v-on="on"
-                              color="success"
-                              v-model="client.enable"
-                              v-on:change="disableClient(client)"
-                      />
-                    </template>
-                    <span> {{client.enable ? 'Disable' : 'Enable'}} this client</span>
-                  </v-tooltip>
-
-                </v-card-actions>
-              </v-card>
-            </v-col>
-          </v-row>
-        </v-card>
-      </v-col>
-    </v-row>
-    <v-dialog
-            v-if="client"
-            v-model="dialogAddClient"
-            max-width="550"
-    >
-      <v-card>
-        <v-card-title class="headline">Add new client</v-card-title>
-        <v-card-text>
-          <v-row>
-            <v-col
-                    cols="12"
-            >
-              <v-form
-                      ref="form"
-                      v-model="valid"
-              >
-                <v-text-field
-                        v-model="client.name"
-                        label="Client friendly name"
-                        :rules="[
-                          v => !!v || 'Client name is required',
-                        ]"
-                        required
-                />
-                <v-text-field
-                        v-model="client.email"
-                        label="Client email"
-                        :rules="[
-                        v => !!v || 'E-mail is required',
-                        v => /.+@.+\..+/.test(v) || 'E-mail must be valid',
-                      ]"
-                        required
-                />
-                <v-select
-                        v-model="clientAddress"
-                        :items="serverAddress"
-                        label="Client IP will be chosen from these networks"
-                        :rules="[
-                                v => !!v || 'Network is required',
-                        ]"
-                        multiple
-                        chips
-                        persistent-hint
-                        required
-                />
-                <v-combobox
-                        v-model="client.allowedIPs"
-                        chips
-                        hint="Write IPv4 or IPv6 CIDR and hit enter"
-                        label="Allowed IPs"
-                        multiple
-                        dark
-                >
-                  <template v-slot:selection="{ attrs, item, select, selected }">
-                    <v-chip
-                            v-bind="attrs"
-                            :input-value="selected"
-                            close
-                            @click="select"
-                            @click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
-                    >
-                      <strong>{{ item }}</strong>&nbsp;
-                    </v-chip>
-                  </template>
-                </v-combobox>
-
-                <v-switch
-                        v-model="client.enable"
-                        color="red"
-                        inset
-                        :label="client.enable ? 'Enable client after creation': 'Disable client after creation'"
-                />
-              </v-form>
-            </v-col>
-          </v-row>
-        </v-card-text>
-        <v-card-actions>
-          <v-spacer/>
-          <v-btn
-                  :disabled="!valid"
-                  color="success"
-                  @click="addClient(client)"
-          >
-            Submit
-            <v-icon right dark>mdi-check-outline</v-icon>
-          </v-btn>
-          <v-btn
-                  color="primary"
-                  @click="dialogAddClient = false"
-          >
-            Cancel
-            <v-icon right dark>mdi-close-circle-outline</v-icon>
-          </v-btn>
-        </v-card-actions>
-      </v-card>
-    </v-dialog>
-    <v-dialog
-            v-if="client"
-            v-model="dialogEditClient"
-            max-width="550"
-    >
-      <v-card>
-        <v-card-title class="headline">Edit client</v-card-title>
-        <v-card-text>
-          <v-row>
-            <v-col
-                    cols="12"
-            >
-              <v-form
-                      ref="form"
-                      v-model="valid"
-              >
-                <v-text-field
-                        v-model="client.name"
-                        label="Friendly name"
-                        :rules="[
-                          v => !!v || 'Client name is required',
-                        ]"
-                        required
-                />
-                <v-text-field
-                        v-model="client.email"
-                        label="Email"
-                        :rules="[
-                        v => !!v || 'Email is required',
-                        v => /.+@.+\..+/.test(v) || 'Email must be valid',
-                        ]"
-                        required
-                />
-                <v-combobox
-                        v-model="client.address"
-                        chips
-                        hint="Write IPv4 or IPv6 CIDR and hit enter"
-                        label="Addresses"
-                        multiple
-                        dark
-                >
-                  <template v-slot:selection="{ attrs, item, select, selected }">
-                    <v-chip
-                            v-bind="attrs"
-                            :input-value="selected"
-                            close
-                            @click="select"
-                            @click:close="client.address.splice(client.address.indexOf(item), 1)"
-                    >
-                      <strong>{{ item }}</strong>&nbsp;
-                    </v-chip>
-                  </template>
-                </v-combobox>
-                <v-combobox
-                        v-model="client.allowedIPs"
-                        chips
-                        hint="Write IPv4 or IPv6 CIDR and hit enter"
-                        label="Allowed IPs"
-                        multiple
-                        dark
-                >
-                  <template v-slot:selection="{ attrs, item, select, selected }">
-                    <v-chip
-                            v-bind="attrs"
-                            :input-value="selected"
-                            close
-                            @click="select"
-                            @click:close="client.allowedIPs.splice(client.allowedIPs.indexOf(item), 1)"
-                    >
-                      <strong>{{ item }}</strong>&nbsp;
-                    </v-chip>
-                  </template>
-                </v-combobox>
-              </v-form>
-            </v-col>
-          </v-row>
-        </v-card-text>
-        <v-card-actions>
-          <v-spacer/>
-          <v-btn
-                  :disabled="!valid"
-                  color="success"
-                  @click="updateClient(client)"
-          >
-            Submit
-            <v-icon right dark>mdi-check-outline</v-icon>
-          </v-btn>
-          <v-btn
-                  color="primary"
-                  @click="dialogEditClient = false"
-          >
-            Cancel
-            <v-icon right dark>mdi-close-circle-outline</v-icon>
-          </v-btn>
-        </v-card-actions>
-      </v-card>
-    </v-dialog>
-    <v-snackbar
-            v-model="notification.show"
-            :right="true"
-            :top="true"
-            :color="notification.color"
-    >
-      {{ notification.text }}
-      <v-btn
-              dark
-              text
-              @click="notification.show = false"
-      >
-        Close
-      </v-btn>
-    </v-snackbar>
+    <Server/>
+    <Clients/>
   </v-content>
 </template>
 
 <script>
+  import Server from '../components/Server'
+  import Clients from '../components/Clients'
+
   export default {
     name: 'home',
-    mounted () {
-      this.getData()
-    },
-    data: () => ({
-      notification: {
-        show: false,
-        color: '',
-        text: '',
-      },
-      valid: true,
-      checkbox: false,
-      server: null,
-      clients: [],
-      ipDns: "",
-      ipAddress: "",
-      clientAddress: [],
-      serverAddress: [],
-      dialogAddClient: false,
-      dialogEditClient: false,
-      client: null,
-    }),
-    methods: {
-      startAddClient() {
-        this.dialogAddClient = true;
-        this.client = {
-          name: "",
-          email: "",
-          enable: true,
-          allowedIPs: ["0.0.0.0/0", "::/0"],
-          address: "",
-        }
-      },
-      editClient(id) {
-        this.$get(`/client/${id}`).then((res) => {
-          this.dialogEditClient = true;
-          res.allowedIPs = res.allowedIPs.split(',');
-          res.address = res.address.split(',');
-          this.client = res
-        }).catch((e) => {
-          this.notify('error', e.response.status + ' ' + e.response.statusText);
-        });
-      },
-      disableClient(client) {
-        if(!Array.isArray(client.allowedIPs)){
-          client.allowedIPs = client.allowedIPs.split(',');
-        }
-        if(!Array.isArray(client.address)){
-          client.address = client.address.split(',');
-        }
-        this.updateClient(client)
-      },
-      getData() {
-        this.$get('/server').then((res) => {
-          res.address = res.address.split(',');
-          res.dns = res.dns.split(',');
-          this.server = res;
-          this.clientAddress = this.serverAddress = this.server.address
-        }).catch((e) => {
-          this.notify('error', e.response.status + ' ' + e.response.statusText);
-        });
-
-        this.$get('/client').then((res) => {
-          this.clients = res
-        }).catch((e) => {
-          this.notify('error', e.response.status + ' ' + e.response.statusText);
-        });
-      },
-      updateServer () {
-        // convert int values
-        this.server.listenPort = parseInt(this.server.listenPort, 10);
-        this.server.persistentKeepalive = parseInt(this.server.persistentKeepalive, 10);
-        // check server addresses
-        if (this.server.address.length < 1) {
-          this.notify('error', 'Please provide at least one valid CIDR address for server interface');
-          return;
-        }
-        for (let i = 0; i < this.server.address.length; i++){
-          if (this.$isCidr(this.server.address[i]) === 0) {
-            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
-            return
-          }
-        }
-        this.server.address = this.server.address.join(',');
-        this.server.dns = this.server.dns.join(',');
-
-        this.$patch('/server', this.server).then((res) => {
-          this.notify('success', "Server successfully updated");
-          this.getData()
-        }).catch((e) => {
-          this.notify('error', e.response.status + ' ' + e.response.statusText);
-        });
-      },
-      addClient(client) {
-        if (client.allowedIPs.length < 1) {
-          this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
-          return;
-        }
-        for (let i = 0; i < client.allowedIPs.length; i++){
-          if (this.$isCidr(client.allowedIPs[i]) === 0) {
-            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
-            return
-          }
-        }
-
-        this.dialogAddClient = false;
-        client.address = this.clientAddress.join(',');
-        client.allowedIPs = this.client.allowedIPs.join(',');
-
-        this.$post('/client', client).then((res) => {
-          this.notify('success', "Client successfully added");
-          this.getData()
-        }).catch((e) => {
-          this.notify('error', e.response.status + ' ' + e.response.statusText);
-        });
-      },
-      deleteClient(id) {
-        if(confirm("Do you really want to delete?")){
-          this.$delete(`/client/${id}`).then((res) => {
-            this.notify('success', "Client successfully deleted");
-            this.getData()
-          }).catch((e) => {
-            this.notify('error', e.response.status + ' ' + e.response.statusText);
-          });
-        }
-      },
-      sendEmailClient(id) {
-        this.$get(`/client/${id}/email`).then((res) => {
-          this.notify('success', "Email successfully sent");
-          this.getData()
-        }).catch((e) => {
-          this.notify('error', e.response.status + ' ' + e.response.statusText);
-        });
-      },
-      getUrlToConfig(id, qrcode){
-        let base = "/api/v1.0";
-        if (process.env.NODE_ENV === "development"){
-          base = process.env.VUE_APP_API_BASE_URL
-        }
-        if (qrcode){
-          return `${base}/client/${id}/config?qrcode=true`
-        } else {
-          return `${base}/client/${id}/config`
-        }
-      },
-      updateClient(client) {
-        // check allowed IPs
-        if (client.allowedIPs.length < 1) {
-          this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
-          return;
-        }
-        for (let i = 0; i < client.allowedIPs.length; i++){
-          if (this.$isCidr(client.allowedIPs[i]) === 0) {
-            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
-            return
-          }
-        }
-        // check address
-        if (client.address.length < 1) {
-          this.notify('error', 'Please provide at least one valid CIDR address for client');
-          return;
-        }
-        for (let i = 0; i < client.address.length; i++){
-          if (this.$isCidr(client.address[i]) === 0) {
-            this.notify('error', 'Invalid CIDR detected, please correct before submitting');
-            return
-          }
-        }
-        // all good, submit
-        this.dialogEditClient = false;
-        client.allowedIPs = client.allowedIPs.join(',');
-        client.address = client.address.join(',');
-
-        this.$patch(`/client/${client.id}`, client).then((res) => {
-          this.notify('success', "Client successfully updated");
-          this.getData()
-        }).catch((e) => {
-          this.notify('error', e.response.status + ' ' + e.response.statusText);
-        });
-      },
-      notify(color, msg) {
-        this.notification.show = true;
-        this.notification.color = color;
-        this.notification.text = msg;
-      },
-
-    },
+    components: {
+      Server,
+      Clients
+    }
   }
 </script>
-- 
GitLab