diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a17c0c5e0a6123282a8a10f62965947362518036..4865708773dfcd06591d2f82dbd1d1fce8ee0daa 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 af50f9ccfea5149bc814b31cf81513cec4da552b..0123076372509c3633307b045ed37c755b357211 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 e2839fcd5ead7318185e2a349681cff44d00acdd..2a6ddece98a0486aee32db217c6c2e1ce2030292 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 0000000000000000000000000000000000000000..e8e11378e9f317d75ee64b6a2bd1530612201dfb --- /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 a4535d794927d85d2e76dab6089e45bc2cca0690..6a94f335f7f75b857fa403507ce27ad5831d2144 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 a5cdd968a81162ef2227179cc00df4b340dc0605..737256ab9861bc4018cf1301b813f46789fe061a 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 3ff5dcba2e0f83020c0bea28fa804592ae60041c..eb0eb77044b013aeb6415fe0237e31a690f36338 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 cb52ce02116d6289c441274152345fe93caf3ac7..1007a665ed7ab02c06f611676dba4d980ea4f43e 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 ea6a72077280928a0bc80e63027748dc2d521f7d..007a0831a2fafd0bd67726dfe33eb061fb6ab681 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 6a3bf90cb066a417153b064600fec466be0812f9..5a594d82a1da2bf4485d0462b7e79e5e94b0156d 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 82eb32803489b1a2f93196adc3df5be00dfdfbd0..64aff469864f89d8328a5731333ddd2708378aa1 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 0000000000000000000000000000000000000000..71f71f92c119f723617f0562511493dec0c7a916 --- /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> + </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> + </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> + </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 0000000000000000000000000000000000000000..a898aa6ec7c1a3c85cb9a79bec9903a1e3a4b8ef --- /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 0000000000000000000000000000000000000000..01e910616c60e556f3a0bcd4f2cc21a5d97a1236 --- /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> + </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> + </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 6ba418755b8dbfecc00f77f704b303d12966f1e1..bfb2c507e3d5233979b13bdaeb0b7341de2b7990 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 11c1ce53722b4a76e913625e3409174af7408f7f..0000000000000000000000000000000000000000 --- 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 b03fce73668e44ff55dfb2f3299cc9fba759a382..5e8ebb0146886400918ec6e8d089efb809b9819f 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 0000000000000000000000000000000000000000..dc55572b974b1acb4e294a1ccfca6d20af70a6e9 --- /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 7a6d2db3c8e4c819d054c5d49cb7edcbcde226c5..6ac0b8a0df6a5bb7e6179530699d15102df73f17 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> - </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> - </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> - </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> - </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> - </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>