mirror of
https://github.com/vx3r/wg-gen-web.git
synced 2024-11-04 17:31:17 +00:00
oauth2 oidc, vuex store
This commit is contained in:
parent
f90124afbf
commit
125ddaef0f
35
.env
35
.env
@ -1,12 +1,41 @@
|
||||
# IP address to listen to
|
||||
SERVER=0.0.0.0
|
||||
# port to bind
|
||||
PORT=8080
|
||||
# Gin framework release mode
|
||||
GIN_MODE=release
|
||||
|
||||
# where to write all generated config files
|
||||
WG_CONF_DIR=./wireguard
|
||||
# WireGuard main config file name, generally <interface name>.conf
|
||||
WG_INTERFACE_NAME=wg0.conf
|
||||
|
||||
# SMTP settings to send email to clients
|
||||
SMTP_HOST=smtp.gmail.com
|
||||
SMTP_PORT=587
|
||||
SMTP_USERNAME=account@gmail.com
|
||||
SMTP_PASSWORD="*************"
|
||||
SMTP_FROM="Wg Gen Web <account@gmail.com>"
|
||||
SMTP_PASSWORD=*************
|
||||
SMTP_FROM=Wg Gen Web <account@gmail.com>
|
||||
|
||||
# example with gitlab, which is RFC implementation and no need any custom stuff
|
||||
OAUTH2_PROVIDER_NAME=oauth2oidc
|
||||
OAUTH2_PROVIDER=https://gitlab.com
|
||||
OAUTH2_CLIENT_ID=
|
||||
OAUTH2_CLIENT_SECRET=
|
||||
OAUTH2_REDIRECT_URL=https://wg-gen-web-demo.127-0-0-1.fr
|
||||
|
||||
# example with google
|
||||
OAUTH2_PROVIDER_NAME=google
|
||||
OAUTH2_PROVIDER=
|
||||
OAUTH2_CLIENT_ID=
|
||||
OAUTH2_CLIENT_SECRET=
|
||||
OAUTH2_REDIRECT_URL=
|
||||
|
||||
# example with github
|
||||
OAUTH2_PROVIDER_NAME=github
|
||||
OAUTH2_PROVIDER=
|
||||
OAUTH2_CLIENT_ID=
|
||||
OAUTH2_CLIENT_SECRET=
|
||||
OAUTH2_REDIRECT_URL=
|
||||
|
||||
# set provider name to fake to disable auth, also the default
|
||||
OAUTH2_PROVIDER_NAME=fake
|
||||
|
240
api/api.go
240
api/api.go
@ -2,244 +2,12 @@ package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/version"
|
||||
"net/http"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1"
|
||||
)
|
||||
|
||||
// ApplyRoutes applies router to gin Router
|
||||
func ApplyRoutes(r *gin.Engine) {
|
||||
client := r.Group("/api/v1.0/client")
|
||||
func ApplyRoutes(r *gin.Engine, private bool) {
|
||||
api := r.Group("/api")
|
||||
{
|
||||
|
||||
client.POST("", createClient)
|
||||
client.GET("/:id", readClient)
|
||||
client.PATCH("/:id", updateClient)
|
||||
client.DELETE("/:id", deleteClient)
|
||||
client.GET("", readClients)
|
||||
client.GET("/:id/config", configClient)
|
||||
client.GET("/:id/email", emailClient)
|
||||
}
|
||||
|
||||
server := r.Group("/api/v1.0/server")
|
||||
{
|
||||
server.GET("", readServer)
|
||||
server.PATCH("", updateServer)
|
||||
server.GET("/config", configServer)
|
||||
server.GET("/version", versionStr)
|
||||
apiv1.ApplyRoutes(api, private)
|
||||
}
|
||||
}
|
||||
|
||||
func createClient(c *gin.Context) {
|
||||
var data model.Client
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to bind")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := core.CreateClient(&data)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to create client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, client)
|
||||
}
|
||||
|
||||
func readClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
client, err := core.ReadClient(id)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, client)
|
||||
}
|
||||
|
||||
func updateClient(c *gin.Context) {
|
||||
var data model.Client
|
||||
id := c.Param("id")
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to bind")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := core.UpdateClient(id, &data)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to update client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, client)
|
||||
}
|
||||
|
||||
func deleteClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
err := core.DeleteClient(id)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to remove client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func readClients(c *gin.Context) {
|
||||
clients, err := core.ReadClients()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to list clients")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, clients)
|
||||
}
|
||||
|
||||
func configClient(c *gin.Context) {
|
||||
configData, err := core.ReadClientConfig(c.Param("id"))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read client config")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
formatQr := c.DefaultQuery("qrcode", "false")
|
||||
if formatQr == "false" {
|
||||
// return config as txt file
|
||||
c.Header("Content-Disposition", "attachment; filename=wg0.conf")
|
||||
c.Data(http.StatusOK, "application/config", configData)
|
||||
return
|
||||
}
|
||||
// return config as png qrcode
|
||||
png, err := qrcode.Encode(string(configData), qrcode.Medium, 250)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to create qrcode")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "image/png", png)
|
||||
return
|
||||
}
|
||||
|
||||
func emailClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
err := core.EmailClient(id)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to send email to client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func readServer(c *gin.Context) {
|
||||
client, err := core.ReadServer()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, client)
|
||||
}
|
||||
|
||||
func updateServer(c *gin.Context) {
|
||||
var data model.Server
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to bind")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
client, err := core.UpdateServer(&data)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to update client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, client)
|
||||
}
|
||||
|
||||
func configServer(c *gin.Context) {
|
||||
clients, err := core.ReadClients()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read clients")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
server, err := core.ReadServer()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read server")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := template.DumpServerWg(clients, server)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to dump wg config")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
// return config as txt file
|
||||
c.Header("Content-Disposition", "attachment; filename=wg0.conf")
|
||||
c.Data(http.StatusOK, "application/config", configData)
|
||||
}
|
||||
|
||||
func versionStr(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": version.Version,
|
||||
})
|
||||
}
|
||||
|
134
api/v1/auth/auth.go
Normal file
134
api/v1/auth/auth.go
Normal file
@ -0,0 +1,134 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/patrickmn/go-cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/auth"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||
"golang.org/x/oauth2"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ApplyRoutes applies router to gin Router
|
||||
func ApplyRoutes(r *gin.RouterGroup) {
|
||||
g := r.Group("/auth")
|
||||
{
|
||||
g.GET("/oauth2_url", oauth2_url)
|
||||
g.POST("/oauth2_exchange", oauth2_exchange)
|
||||
g.GET("/user", user)
|
||||
g.GET("/logout", logout)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* generate redirect url to get OAuth2 code or let client know that OAuth2 is disabled
|
||||
*/
|
||||
func oauth2_url(c *gin.Context) {
|
||||
cacheDb := c.MustGet("cache").(*cache.Cache)
|
||||
|
||||
state, err := util.GenerateRandomString(32)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to generate state random string")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
clientId, err := util.GenerateRandomString(32)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to generate state random string")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
// save clientId and state so we can retrieve for verification
|
||||
cacheDb.Set(clientId, state, 5*time.Minute)
|
||||
|
||||
oauth2Client := c.MustGet("oauth2Client").(auth.Auth)
|
||||
|
||||
data := &model.Auth{
|
||||
Oauth2: true,
|
||||
ClientId: clientId,
|
||||
State: state,
|
||||
CodeUrl: oauth2Client.CodeUrl(state),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, data)
|
||||
}
|
||||
|
||||
/*
|
||||
* exchange code and get user infos, if OAuth2 is disable just send fake data
|
||||
*/
|
||||
func oauth2_exchange(c *gin.Context) {
|
||||
var loginVals model.Auth
|
||||
if err := c.ShouldBind(&loginVals); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("code and state fields are missing")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
cacheDb := c.MustGet("cache").(*cache.Cache)
|
||||
savedState, exists := cacheDb.Get(loginVals.ClientId)
|
||||
|
||||
if !exists || savedState != loginVals.State {
|
||||
log.WithFields(log.Fields{
|
||||
"state": loginVals.State,
|
||||
"savedState": savedState,
|
||||
}).Error("saved state and client provided state mismatch")
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
oauth2Client := c.MustGet("oauth2Client").(auth.Auth)
|
||||
|
||||
oauth2Token, err := oauth2Client.Exchange(loginVals.Code)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to exchange code for token")
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cacheDb.Delete(loginVals.ClientId)
|
||||
cacheDb.Set(oauth2Token.AccessToken, oauth2Token, cache.DefaultExpiration)
|
||||
|
||||
c.JSON(http.StatusOK, oauth2Token.AccessToken)
|
||||
}
|
||||
|
||||
func logout(c *gin.Context) {
|
||||
cacheDb := c.MustGet("cache").(*cache.Cache)
|
||||
cacheDb.Delete(c.Request.Header.Get(util.AuthTokenHeaderName))
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func user(c *gin.Context) {
|
||||
cacheDb := c.MustGet("cache").(*cache.Cache)
|
||||
oauth2Token, exists := cacheDb.Get(c.Request.Header.Get(util.AuthTokenHeaderName))
|
||||
|
||||
if exists && oauth2Token.(*oauth2.Token).AccessToken == c.Request.Header.Get(util.AuthTokenHeaderName) {
|
||||
oauth2Client := c.MustGet("oauth2Client").(auth.Auth)
|
||||
user, err := oauth2Client.UserInfo(oauth2Token.(*oauth2.Token))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to get user from oauth2 AccessToken")
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
return
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"exists": exists,
|
||||
util.AuthTokenHeaderName: c.Request.Header.Get(util.AuthTokenHeaderName),
|
||||
}).Error("oauth2 AccessToken is not recognized")
|
||||
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
}
|
190
api/v1/client/client.go
Normal file
190
api/v1/client/client.go
Normal file
@ -0,0 +1,190 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/auth"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||
"golang.org/x/oauth2"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ApplyRoutes applies router to gin Router
|
||||
func ApplyRoutes(r *gin.RouterGroup) {
|
||||
g := r.Group("/client")
|
||||
{
|
||||
|
||||
g.POST("", createClient)
|
||||
g.GET("/:id", readClient)
|
||||
g.PATCH("/:id", updateClient)
|
||||
g.DELETE("/:id", deleteClient)
|
||||
g.GET("", readClients)
|
||||
g.GET("/:id/config", configClient)
|
||||
g.GET("/:id/email", emailClient)
|
||||
}
|
||||
}
|
||||
|
||||
func createClient(c *gin.Context) {
|
||||
var data model.Client
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to bind")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
// get creation user from token and add to client infos
|
||||
oauth2Token := c.MustGet("oauth2Token").(*oauth2.Token)
|
||||
oauth2Client := c.MustGet("oauth2Client").(auth.Auth)
|
||||
user, err := oauth2Client.UserInfo(oauth2Token)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"oauth2Token": oauth2Token,
|
||||
"err": err,
|
||||
}).Error("failed to get user with oauth token")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data.CreatedBy = user.Name
|
||||
|
||||
client, err := core.CreateClient(&data)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to create client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, client)
|
||||
}
|
||||
|
||||
func readClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
client, err := core.ReadClient(id)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, client)
|
||||
}
|
||||
|
||||
func updateClient(c *gin.Context) {
|
||||
var data model.Client
|
||||
id := c.Param("id")
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to bind")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
// get update user from token and add to client infos
|
||||
oauth2Token := c.MustGet("oauth2Token").(*oauth2.Token)
|
||||
oauth2Client := c.MustGet("oauth2Client").(auth.Auth)
|
||||
user, err := oauth2Client.UserInfo(oauth2Token)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"oauth2Token": oauth2Token,
|
||||
"err": err,
|
||||
}).Error("failed to get user with oauth token")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data.UpdatedBy = user.Name
|
||||
|
||||
client, err := core.UpdateClient(id, &data)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to update client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, client)
|
||||
}
|
||||
|
||||
func deleteClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
err := core.DeleteClient(id)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to remove client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
||||
|
||||
func readClients(c *gin.Context) {
|
||||
clients, err := core.ReadClients()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to list clients")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, clients)
|
||||
}
|
||||
|
||||
func configClient(c *gin.Context) {
|
||||
configData, err := core.ReadClientConfig(c.Param("id"))
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read client config")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
formatQr := c.DefaultQuery("qrcode", "false")
|
||||
if formatQr == "false" {
|
||||
// return config as txt file
|
||||
c.Header("Content-Disposition", "attachment; filename=wg0.conf")
|
||||
c.Data(http.StatusOK, "application/config", configData)
|
||||
return
|
||||
}
|
||||
// return config as png qrcode
|
||||
png, err := qrcode.Encode(string(configData), qrcode.Medium, 250)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to create qrcode")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "image/png", png)
|
||||
return
|
||||
}
|
||||
|
||||
func emailClient(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
err := core.EmailClient(id)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to send email to client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{})
|
||||
}
|
113
api/v1/server/server.go
Normal file
113
api/v1/server/server.go
Normal file
@ -0,0 +1,113 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/auth"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/template"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/version"
|
||||
"golang.org/x/oauth2"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ApplyRoutes applies router to gin Router
|
||||
func ApplyRoutes(r *gin.RouterGroup) {
|
||||
g := r.Group("/server")
|
||||
{
|
||||
g.GET("", readServer)
|
||||
g.PATCH("", updateServer)
|
||||
g.GET("/config", configServer)
|
||||
g.GET("/version", versionStr)
|
||||
}
|
||||
}
|
||||
|
||||
func readServer(c *gin.Context) {
|
||||
client, err := core.ReadServer()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, client)
|
||||
}
|
||||
|
||||
func updateServer(c *gin.Context) {
|
||||
var data model.Server
|
||||
|
||||
if err := c.ShouldBindJSON(&data); err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to bind")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
// get update user from token and add to server infos
|
||||
oauth2Token := c.MustGet("oauth2Token").(*oauth2.Token)
|
||||
oauth2Client := c.MustGet("oauth2Client").(auth.Auth)
|
||||
user, err := oauth2Client.UserInfo(oauth2Token)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"oauth2Token": oauth2Token,
|
||||
"err": err,
|
||||
}).Error("failed to get user with oauth token")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data.UpdatedBy = user.Name
|
||||
|
||||
server, err := core.UpdateServer(&data)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to update client")
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, server)
|
||||
}
|
||||
|
||||
func configServer(c *gin.Context) {
|
||||
clients, err := core.ReadClients()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read clients")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
server, err := core.ReadServer()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to read server")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
configData, err := template.DumpServerWg(clients, server)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Error("failed to dump wg config")
|
||||
c.AbortWithStatus(http.StatusUnprocessableEntity)
|
||||
return
|
||||
}
|
||||
|
||||
// return config as txt file
|
||||
c.Header("Content-Disposition", "attachment; filename=wg0.conf")
|
||||
c.Data(http.StatusOK, "application/config", configData)
|
||||
}
|
||||
|
||||
func versionStr(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"version": version.Version,
|
||||
})
|
||||
}
|
21
api/v1/v1.go
Normal file
21
api/v1/v1.go
Normal file
@ -0,0 +1,21 @@
|
||||
package apiv1
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/auth"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/client"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/api/v1/server"
|
||||
)
|
||||
|
||||
func ApplyRoutes(r *gin.RouterGroup, private bool) {
|
||||
v1 := r.Group("/v1.0")
|
||||
{
|
||||
if private {
|
||||
client.ApplyRoutes(v1)
|
||||
server.ApplyRoutes(v1)
|
||||
} else {
|
||||
auth.ApplyRoutes(v1)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
13
auth/auth.go
Normal file
13
auth/auth.go
Normal file
@ -0,0 +1,13 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type Auth interface {
|
||||
Setup() error
|
||||
CodeUrl(state string) string
|
||||
Exchange(code string) (*oauth2.Token, error)
|
||||
UserInfo(oauth2Token *oauth2.Token) (*model.User, error)
|
||||
}
|
47
auth/fake/fake.go
Normal file
47
auth/fake/fake.go
Normal file
@ -0,0 +1,47 @@
|
||||
package fake
|
||||
|
||||
import (
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||
"golang.org/x/oauth2"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Fake struct{}
|
||||
|
||||
// Setup validate provider
|
||||
func (o *Fake) Setup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CodeUrl get url to redirect client for auth
|
||||
func (o *Fake) CodeUrl(state string) string {
|
||||
return "_magic_string_fake_auth_no_redirect_"
|
||||
}
|
||||
|
||||
// Exchange exchange code for Oauth2 token
|
||||
func (o *Fake) Exchange(code string) (*oauth2.Token, error) {
|
||||
rand, err := util.GenerateRandomString(32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &oauth2.Token{
|
||||
AccessToken: rand,
|
||||
TokenType: "",
|
||||
RefreshToken: "",
|
||||
Expiry: time.Time{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UserInfo get token user
|
||||
func (o *Fake) UserInfo(oauth2Token *oauth2.Token) (*model.User, error) {
|
||||
return &model.User{
|
||||
Sub: "unknown",
|
||||
Name: "Unknown",
|
||||
Email: "unknown",
|
||||
Profile: "unknown",
|
||||
Issuer: "unknown",
|
||||
IssuedAt: time.Time{},
|
||||
}, nil
|
||||
}
|
1
auth/github/github.go
Normal file
1
auth/github/github.go
Normal file
@ -0,0 +1 @@
|
||||
package github
|
1
auth/google/goolge.go
Normal file
1
auth/google/goolge.go
Normal file
@ -0,0 +1 @@
|
||||
package google
|
108
auth/oauth2oidc/oauth2oidc.go
Normal file
108
auth/oauth2oidc/oauth2oidc.go
Normal file
@ -0,0 +1,108 @@
|
||||
package oauth2oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/coreos/go-oidc"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/model"
|
||||
"golang.org/x/oauth2"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Oauth2idc struct{}
|
||||
|
||||
var (
|
||||
oauth2Config *oauth2.Config
|
||||
oidcProvider *oidc.Provider
|
||||
oidcIDTokenVerifier *oidc.IDTokenVerifier
|
||||
)
|
||||
|
||||
// Setup validate provider
|
||||
func (o *Oauth2idc) Setup() error {
|
||||
var err error
|
||||
|
||||
oidcProvider, err = oidc.NewProvider(context.TODO(), os.Getenv("OAUTH2_PROVIDER"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
oidcIDTokenVerifier = oidcProvider.Verifier(&oidc.Config{
|
||||
ClientID: os.Getenv("OAUTH2_CLIENT_ID"),
|
||||
})
|
||||
|
||||
oauth2Config = &oauth2.Config{
|
||||
ClientID: os.Getenv("OAUTH2_CLIENT_ID"),
|
||||
ClientSecret: os.Getenv("OAUTH2_CLIENT_SECRET"),
|
||||
RedirectURL: os.Getenv("OAUTH2_REDIRECT_URL"),
|
||||
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
|
||||
Endpoint: oidcProvider.Endpoint(),
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CodeUrl get url to redirect client for auth
|
||||
func (o *Oauth2idc) CodeUrl(state string) string {
|
||||
return oauth2Config.AuthCodeURL(state)
|
||||
}
|
||||
|
||||
// Exchange exchange code for Oauth2 token
|
||||
func (o *Oauth2idc) Exchange(code string) (*oauth2.Token, error) {
|
||||
oauth2Token, err := oauth2Config.Exchange(context.TODO(), code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return oauth2Token, nil
|
||||
}
|
||||
|
||||
// UserInfo get token user
|
||||
func (o *Oauth2idc) UserInfo(oauth2Token *oauth2.Token) (*model.User, error) {
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no id_token field in oauth2 token")
|
||||
}
|
||||
|
||||
iDToken, err := oidcIDTokenVerifier.Verify(context.TODO(), rawIDToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userInfo, err := oidcProvider.UserInfo(context.TODO(), oauth2.StaticTokenSource(oauth2Token))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type UserInfo struct {
|
||||
Subject string `json:"sub"`
|
||||
Profile string `json:"profile"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"email_verified"`
|
||||
|
||||
claims []byte
|
||||
}
|
||||
|
||||
// ID Token payload is just JSON
|
||||
var claims map[string]interface{}
|
||||
if err := userInfo.Claims(&claims); err != nil {
|
||||
return nil, fmt.Errorf("failed to get id token claims: %s", err)
|
||||
}
|
||||
|
||||
// get some infos about user
|
||||
user := &model.User{}
|
||||
user.Sub = userInfo.Subject
|
||||
user.Email = userInfo.Email
|
||||
user.Profile = userInfo.Profile
|
||||
|
||||
if v, found := claims["name"]; found && v != nil {
|
||||
user.Name = v.(string)
|
||||
} else {
|
||||
log.Error("name not found in user info claims")
|
||||
}
|
||||
|
||||
user.Issuer = iDToken.Issuer
|
||||
user.IssuedAt = iDToken.IssuedAt
|
||||
|
||||
return user, nil
|
||||
}
|
@ -7,13 +7,24 @@ import (
|
||||
"github.com/gin-contrib/static"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/patrickmn/go-cache"
|
||||
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/auth"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/auth/fake"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/auth/oauth2oidc"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/core"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/util"
|
||||
"gitlab.127-0-0-1.fr/vx3r/wg-gen-web/version"
|
||||
"golang.org/x/oauth2"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
cacheDb = cache.New(60*time.Minute, 10*time.Minute)
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -80,21 +91,88 @@ func main() {
|
||||
// cors middleware
|
||||
config := cors.DefaultConfig()
|
||||
config.AllowAllOrigins = true
|
||||
config.AddAllowHeaders("Authorization")
|
||||
app.Use(cors.New(config))
|
||||
|
||||
// protection middleware
|
||||
app.Use(helmet.Default())
|
||||
|
||||
// no route redirect to frontend app
|
||||
app.NoRoute(func(c *gin.Context) {
|
||||
c.Redirect(301, "/index.html")
|
||||
// add cache storage to gin app
|
||||
app.Use(func(ctx *gin.Context) {
|
||||
ctx.Set("cache", cacheDb)
|
||||
ctx.Next()
|
||||
})
|
||||
|
||||
// serve static files
|
||||
app.Use(static.Serve("/", static.LocalFile("./ui/dist", false)))
|
||||
|
||||
// apply api router
|
||||
api.ApplyRoutes(app)
|
||||
// setup Oauth2 client if enabled
|
||||
var oauth2Client auth.Auth
|
||||
|
||||
switch os.Getenv("OAUTH2_PROVIDER_NAME") {
|
||||
case "fake":
|
||||
log.Warn("Oauth is set to fake, no actual authentication will be performed")
|
||||
oauth2Client = &fake.Fake{}
|
||||
err = oauth2Client.Setup()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"OAUTH2_PROVIDER_NAME": "oauth2oidc",
|
||||
"err": err,
|
||||
}).Fatal("failed to setup Oauth2")
|
||||
}
|
||||
case "oauth2oidc":
|
||||
log.Warn("Oauth is set to oauth2oidc, must be RFC implementation on server side")
|
||||
oauth2Client = &oauth2oidc.Oauth2idc{}
|
||||
err = oauth2Client.Setup()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"OAUTH2_PROVIDER_NAME": "oauth2oidc",
|
||||
"err": err,
|
||||
}).Fatal("failed to setup Oauth2")
|
||||
}
|
||||
default:
|
||||
log.WithFields(log.Fields{
|
||||
"OAUTH2_PROVIDER_NAME": os.Getenv("OAUTH2_PROVIDER_NAME"),
|
||||
}).Fatal("auth provider name unknown")
|
||||
}
|
||||
|
||||
if os.Getenv("OAUTH2_ENABLE") == "true" {
|
||||
oauth2Client = &oauth2oidc.Oauth2idc{}
|
||||
err = oauth2Client.Setup()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{
|
||||
"err": err,
|
||||
}).Fatal("failed to setup Oauth2")
|
||||
}
|
||||
}
|
||||
app.Use(func(ctx *gin.Context) {
|
||||
ctx.Set("oauth2Client", oauth2Client)
|
||||
ctx.Next()
|
||||
})
|
||||
|
||||
// apply api routes public
|
||||
api.ApplyRoutes(app, false)
|
||||
|
||||
// simple middleware to check auth
|
||||
app.Use(func(c *gin.Context) {
|
||||
cacheDb := c.MustGet("cache").(*cache.Cache)
|
||||
|
||||
token := c.Request.Header.Get(util.AuthTokenHeaderName)
|
||||
|
||||
oauth2Token, exists := cacheDb.Get(token)
|
||||
if exists && oauth2Token.(*oauth2.Token).AccessToken == token {
|
||||
// will be accessible in auth endpoints
|
||||
c.Set("oauth2Token", oauth2Token)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
c.AbortWithStatus(http.StatusUnauthorized)
|
||||
return
|
||||
})
|
||||
|
||||
// apply api router private
|
||||
api.ApplyRoutes(app, true)
|
||||
|
||||
err = app.Run(fmt.Sprintf("%s:%s", os.Getenv("SERVER"), os.Getenv("PORT")))
|
||||
if err != nil {
|
||||
|
11
go.mod
11
go.mod
@ -3,15 +3,26 @@ module gitlab.127-0-0-1.fr/vx3r/wg-gen-web
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/appleboy/gin-jwt/v2 v2.6.3 // indirect
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect
|
||||
github.com/coreos/go-oidc v2.2.1+incompatible
|
||||
github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e
|
||||
github.com/gin-contrib/cors v1.3.1
|
||||
github.com/gin-contrib/sessions v0.0.3
|
||||
github.com/gin-contrib/static v0.0.0-20191128031702-f81c604d8ac2
|
||||
github.com/gin-gonic/contrib v0.0.0-20191209060500-d6e26eeaa607 // indirect
|
||||
github.com/gin-gonic/gin v1.6.2
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
|
||||
github.com/gorilla/sessions v1.2.0 // indirect
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/sirupsen/logrus v1.5.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/square/go-jose.v2 v2.5.0 // indirect
|
||||
)
|
||||
|
9
model/auth.go
Normal file
9
model/auth.go
Normal file
@ -0,0 +1,9 @@
|
||||
package model
|
||||
|
||||
type Auth struct {
|
||||
Oauth2 bool `json:"oauth2"`
|
||||
ClientId string `json:"clientId"`
|
||||
Code string `json:"code"`
|
||||
State string `json:"state"`
|
||||
CodeUrl string `json:"codeUrl"`
|
||||
}
|
@ -18,6 +18,8 @@ type Client struct {
|
||||
Address []string `json:"address"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
CreatedBy string `json:"createdBy"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ type Server struct {
|
||||
PostUp string `json:"postUp"`
|
||||
PreDown string `json:"preDown"`
|
||||
PostDown string `json:"postDown"`
|
||||
UpdatedBy string `json:"updatedBy"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
12
model/user.go
Normal file
12
model/user.go
Normal file
@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type User struct {
|
||||
Sub string `json:"sub"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Profile string `json:"profile"`
|
||||
Issuer string `json:"issuer"`
|
||||
IssuedAt time.Time `json:"issuedAt"`
|
||||
}
|
602
ui/package-lock.json
generated
602
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -11,9 +11,11 @@
|
||||
"is-cidr": "^3.1.0",
|
||||
"moment": "^2.24.0",
|
||||
"vue": "^2.6.10",
|
||||
"vue-axios": "^2.1.5",
|
||||
"vue-moment": "^4.1.0",
|
||||
"vue-router": "^3.1.6",
|
||||
"vuetify": "^2.2.22"
|
||||
"vuetify": "^2.2.22",
|
||||
"vuex": "^3.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-router": "^4.3.1",
|
||||
|
129
ui/src/App.vue
129
ui/src/App.vue
@ -1,69 +1,36 @@
|
||||
<template>
|
||||
<v-app id="inspire">
|
||||
<Notification v-bind:notification="notification"/>
|
||||
<div v-if="this.isAuthenticated">
|
||||
<Header/>
|
||||
|
||||
<v-app-bar app>
|
||||
<img class="mr-3" :src="require('./assets/logo.png')" height="50" alt="Wg Gen Web"/>
|
||||
<v-toolbar-title to="/">Wg Gen Web</v-toolbar-title>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-toolbar-items>
|
||||
<v-btn to="/clients">
|
||||
Clients
|
||||
<v-icon right dark>mdi-account-network-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn to="/server">
|
||||
Server
|
||||
<v-icon right dark>mdi-vpn</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
|
||||
</v-app-bar>
|
||||
|
||||
<v-content>
|
||||
<v-container>
|
||||
<router-view />
|
||||
</v-container>
|
||||
<Notification v-bind:notification="notification"/>
|
||||
</v-content>
|
||||
|
||||
<v-footer app>
|
||||
<v-row justify="start" no-gutters>
|
||||
<v-col cols="12" lg="6" md="12" sm="12">
|
||||
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'left'">
|
||||
<small>Copyright © {{ new Date().getFullYear() }}, Wg Gen Web.</small>
|
||||
<small>This work is licensed under a <a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">WTFPL License.</a></small>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row justify="end" no-gutters>
|
||||
<v-col cols="12" lg="6" md="12" sm="12">
|
||||
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'right'">
|
||||
<small>Created with</small>
|
||||
<v-icon class="pr-1 pl-1">mdi-heart</v-icon><span>by</span><a class="pr-2 pl-1" href="mailto:vx3r@127-0-0-1.fr">vx3r</a>
|
||||
<a :href="'https://github.com/vx3r/wg-gen-web/commit/' + version"><kbd>Version: {{ version.substring(0,7) }}</kbd></a>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-footer>
|
||||
<v-content>
|
||||
<v-container>
|
||||
<router-view />
|
||||
</v-container>
|
||||
</v-content>
|
||||
|
||||
<Footer/>
|
||||
</div>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {ApiService} from "./services/ApiService";
|
||||
import Notification from './components/Notification'
|
||||
import Header from "./components/Header";
|
||||
import Footer from "./components/Footer";
|
||||
import {mapActions, mapGetters} from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
|
||||
components: {
|
||||
Footer,
|
||||
Header,
|
||||
Notification
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
api: null,
|
||||
version:'N/A',
|
||||
notification: {
|
||||
show: false,
|
||||
color: '',
|
||||
@ -71,23 +38,69 @@
|
||||
},
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.api = new ApiService();
|
||||
this.getVersion()
|
||||
computed:{
|
||||
...mapGetters({
|
||||
isAuthenticated: 'auth/isAuthenticated',
|
||||
authStatus: 'auth/authStatus',
|
||||
authRedirectUrl: 'auth/authRedirectUrl',
|
||||
authError: 'auth/error',
|
||||
clientError: 'client/error',
|
||||
serverError: 'server/error',
|
||||
})
|
||||
},
|
||||
|
||||
created () {
|
||||
this.$vuetify.theme.dark = true
|
||||
},
|
||||
|
||||
methods: {
|
||||
getVersion() {
|
||||
this.api.get('/server/version').then((res) => {
|
||||
this.version = res.version;
|
||||
}).catch((e) => {
|
||||
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||
});
|
||||
mounted() {
|
||||
if (this.$route.query.code && this.$route.query.state) {
|
||||
this.oauth2_exchange({
|
||||
code: this.$route.query.code,
|
||||
state: this.$route.query.state
|
||||
})
|
||||
} else {
|
||||
this.oauth2_url()
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
authError(newValue, oldValue) {
|
||||
console.log(newValue)
|
||||
this.notify('error', newValue);
|
||||
},
|
||||
|
||||
clientError(newValue, oldValue) {
|
||||
console.log(newValue)
|
||||
this.notify('error', newValue);
|
||||
},
|
||||
|
||||
serverError(newValue, oldValue) {
|
||||
console.log(newValue)
|
||||
this.notify('error', newValue);
|
||||
},
|
||||
|
||||
isAuthenticated(newValue, oldValue) {
|
||||
console.log(`Updating isAuthenticated from ${oldValue} to ${newValue}`);
|
||||
if (newValue === true) {
|
||||
this.$router.push('/clients')
|
||||
}
|
||||
},
|
||||
|
||||
authStatus(newValue, oldValue) {
|
||||
console.log(`Updating authStatus from ${oldValue} to ${newValue}`);
|
||||
if (newValue === 'redirect') {
|
||||
window.location.replace(this.authRedirectUrl)
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('auth', {
|
||||
oauth2_exchange: 'oauth2_exchange',
|
||||
oauth2_url: 'oauth2_url',
|
||||
}),
|
||||
|
||||
notify(color, msg) {
|
||||
this.notification.show = true;
|
||||
this.notification.color = color;
|
||||
|
@ -9,7 +9,7 @@
|
||||
</v-list-item-content>
|
||||
<v-btn
|
||||
color="success"
|
||||
@click="startAddClient"
|
||||
@click="startCreate"
|
||||
>
|
||||
Add new client
|
||||
<v-icon right dark>mdi-account-multiple-plus-outline</v-icon>
|
||||
@ -31,15 +31,15 @@
|
||||
<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-subtitle>Created: {{ client.created | formatDate }} by {{ client.createdBy }}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>Updated: {{ client.updated | formatDate }} by {{ client.updatedBy }}</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-img :src="'data:image/png;base64, ' + getClientQrcode(client.id)"/>
|
||||
</v-list-item-avatar>
|
||||
</v-list-item>
|
||||
|
||||
@ -55,61 +55,61 @@
|
||||
</v-chip>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
:href="`${apiBaseUrl}/client/${client.id}/config?qrcode=false`"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Download</span>
|
||||
<v-icon right dark>mdi-cloud-download-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Download</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
v-on:click="forceFileDownload(client)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Download</span>
|
||||
<v-icon right dark>mdi-cloud-download-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Download</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
@click.stop="startUpdateClient(client)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Edit</span>
|
||||
<v-icon right dark>mdi-square-edit-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Edit</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
@click.stop="startUpdate(client)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Edit</span>
|
||||
<v-icon right dark>mdi-square-edit-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Edit</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
@click="deleteClient(client)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Delete</span>
|
||||
<v-icon right dark>mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Delete</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
@click="remove(client)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Delete</span>
|
||||
<v-icon right dark>mdi-trash-can-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Delete</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
@click="sendEmailClient(client.id)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Send Email</span>
|
||||
<v-icon right dark>mdi-email-send-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Send Email</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn
|
||||
text
|
||||
@click="email(client)"
|
||||
v-on="on"
|
||||
>
|
||||
<span class="d-none d-lg-flex">Send Email</span>
|
||||
<v-icon right dark>mdi-email-send-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>Send Email</span>
|
||||
</v-tooltip>
|
||||
<v-spacer/>
|
||||
<v-tooltip right>
|
||||
<template v-slot:activator="{ on }">
|
||||
@ -118,7 +118,7 @@
|
||||
v-on="on"
|
||||
color="success"
|
||||
v-model="client.enable"
|
||||
v-on:change="updateClient(client)"
|
||||
v-on:change="update(client)"
|
||||
/>
|
||||
</template>
|
||||
<span> {{client.enable ? 'Disable' : 'Enable'}} this client</span>
|
||||
@ -133,7 +133,7 @@
|
||||
</v-row>
|
||||
<v-dialog
|
||||
v-if="client"
|
||||
v-model="dialogAddClient"
|
||||
v-model="dialogCreate"
|
||||
max-width="550"
|
||||
>
|
||||
<v-card>
|
||||
@ -210,14 +210,14 @@
|
||||
<v-btn
|
||||
:disabled="!valid"
|
||||
color="success"
|
||||
@click="addClient(client)"
|
||||
@click="create(client)"
|
||||
>
|
||||
Submit
|
||||
<v-icon right dark>mdi-check-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="dialogAddClient = false"
|
||||
@click="dialogCreate = false"
|
||||
>
|
||||
Cancel
|
||||
<v-icon right dark>mdi-close-circle-outline</v-icon>
|
||||
@ -227,7 +227,7 @@
|
||||
</v-dialog>
|
||||
<v-dialog
|
||||
v-if="client"
|
||||
v-model="dialogEditClient"
|
||||
v-model="dialogUpdate"
|
||||
max-width="550"
|
||||
>
|
||||
<v-card>
|
||||
@ -308,14 +308,14 @@
|
||||
<v-btn
|
||||
:disabled="!valid"
|
||||
color="success"
|
||||
@click="updateClient(client)"
|
||||
@click="update(client)"
|
||||
>
|
||||
Submit
|
||||
<v-icon right dark>mdi-check-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="dialogEditClient = false"
|
||||
@click="dialogUpdate = false"
|
||||
>
|
||||
Cancel
|
||||
<v-icon right dark>mdi-close-circle-outline</v-icon>
|
||||
@ -323,61 +323,50 @@
|
||||
</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'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
||||
export default {
|
||||
name: 'Clients',
|
||||
|
||||
components: {
|
||||
Notification
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
api: null,
|
||||
apiBaseUrl: API_BASE_URL,
|
||||
clients: [],
|
||||
notification: {
|
||||
show: false,
|
||||
color: '',
|
||||
text: '',
|
||||
},
|
||||
dialogAddClient: false,
|
||||
dialogEditClient: false,
|
||||
dialogCreate: false,
|
||||
dialogUpdate: false,
|
||||
client: null,
|
||||
server: null,
|
||||
valid: false,
|
||||
}),
|
||||
|
||||
computed:{
|
||||
...mapGetters({
|
||||
getClientQrcode: 'client/getClientQrcode',
|
||||
getClientConfig: 'client/getClientConfig',
|
||||
server: 'server/server',
|
||||
clients: 'client/clients',
|
||||
clientQrcodes: 'client/clientQrcodes',
|
||||
}),
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.api = new ApiService();
|
||||
this.getClients();
|
||||
this.getServer()
|
||||
this.readAllClients()
|
||||
this.readServer()
|
||||
},
|
||||
|
||||
methods: {
|
||||
getClients() {
|
||||
this.api.get('/client').then((res) => {
|
||||
this.clients = res
|
||||
}).catch((e) => {
|
||||
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||
});
|
||||
},
|
||||
...mapActions('client', {
|
||||
errorClient: 'error',
|
||||
readAllClients: 'readAll',
|
||||
creatClient: 'create',
|
||||
updateClient: 'update',
|
||||
deleteClient: 'delete',
|
||||
emailClient: 'email',
|
||||
}),
|
||||
...mapActions('server', {
|
||||
readServer: 'read',
|
||||
}),
|
||||
|
||||
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;
|
||||
startCreate() {
|
||||
this.client = {
|
||||
name: "",
|
||||
email: "",
|
||||
@ -385,91 +374,87 @@
|
||||
allowedIPs: this.server.allowedips,
|
||||
address: this.server.address,
|
||||
}
|
||||
this.dialogCreate = true;
|
||||
},
|
||||
addClient(client) {
|
||||
|
||||
create(client) {
|
||||
if (client.allowedIPs.length < 1) {
|
||||
this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
|
||||
this.errorClient('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');
|
||||
this.errorClient('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);
|
||||
});
|
||||
this.dialogCreate = false;
|
||||
this.creatClient(client)
|
||||
},
|
||||
|
||||
deleteClient(client) {
|
||||
remove(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);
|
||||
});
|
||||
this.deleteClient(client)
|
||||
}
|
||||
},
|
||||
|
||||
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);
|
||||
});
|
||||
email(client) {
|
||||
if (!client.email){
|
||||
this.errorClient('Client email is not defined')
|
||||
return
|
||||
}
|
||||
|
||||
if(confirm(`Do you really want to send email to ${client.email} with all configurations ?`)){
|
||||
this.emailClient(client)
|
||||
}
|
||||
},
|
||||
|
||||
startUpdateClient(client) {
|
||||
startUpdate(client) {
|
||||
this.client = client;
|
||||
this.dialogEditClient = true;
|
||||
this.dialogUpdate = true;
|
||||
},
|
||||
updateClient(client) {
|
||||
|
||||
update(client) {
|
||||
// check allowed IPs
|
||||
if (client.allowedIPs.length < 1) {
|
||||
this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
|
||||
this.errorClient('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');
|
||||
this.errorClient('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');
|
||||
this.errorClient('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');
|
||||
this.errorClient('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);
|
||||
});
|
||||
this.dialogUpdate = false;
|
||||
this.updateClient(client)
|
||||
},
|
||||
|
||||
notify(color, msg) {
|
||||
this.notification.show = true;
|
||||
this.notification.color = color;
|
||||
this.notification.text = msg;
|
||||
}
|
||||
forceFileDownload(client){
|
||||
let config = this.getClientConfig(client.id)
|
||||
if (!config) {
|
||||
this.errorClient('Failed to download client config');
|
||||
return
|
||||
}
|
||||
const url = window.URL.createObjectURL(new Blob([config]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', 'wg0.conf') //or any other extension
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
},
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
51
ui/src/components/Footer.vue
Normal file
51
ui/src/components/Footer.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-footer app>
|
||||
<v-row justify="start" no-gutters>
|
||||
<v-col cols="12" lg="6" md="12" sm="12">
|
||||
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'left'">
|
||||
<small>Copyright © {{ new Date().getFullYear() }}, Wg Gen Web. </small>
|
||||
<small>This work is licensed under <a class="pr-1 pl-1" href="http://www.wtfpl.net/" target="_blank">WTFPL License.</a></small>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row justify="end" no-gutters>
|
||||
<v-col cols="12" lg="6" md="12" sm="12">
|
||||
<div :align="$vuetify.breakpoint.smAndDown ? 'center' : 'right'">
|
||||
<small>Created with</small>
|
||||
<v-icon class="pr-1 pl-1">mdi-heart</v-icon><span>by</span><a class="pr-2 pl-1" href="mailto:vx3r@127-0-0-1.fr">vx3r</a>
|
||||
<a :href="'https://github.com/vx3r/wg-gen-web/commit/' + version"><kbd>Version: {{ version.substring(0,7) }}</kbd></a>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-footer>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapGetters} from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'Footer',
|
||||
|
||||
data: () => ({
|
||||
|
||||
}),
|
||||
|
||||
computed:{
|
||||
...mapGetters({
|
||||
version: 'server/version',
|
||||
}),
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.versionServer()
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('server', {
|
||||
versionServer: 'version',
|
||||
}),
|
||||
}
|
||||
}
|
||||
</script>
|
77
ui/src/components/Header.vue
Normal file
77
ui/src/components/Header.vue
Normal file
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-app-bar app>
|
||||
<img class="mr-3" :src="require('../assets/logo.png')" height="50" alt="Wg Gen Web"/>
|
||||
<v-toolbar-title to="/">Wg Gen Web</v-toolbar-title>
|
||||
|
||||
<v-spacer />
|
||||
|
||||
<v-toolbar-items>
|
||||
<v-btn to="/clients">
|
||||
Clients
|
||||
<v-icon right dark>mdi-account-network-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn to="/server">
|
||||
Server
|
||||
<v-icon right dark>mdi-vpn</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar-items>
|
||||
|
||||
<v-menu
|
||||
left
|
||||
bottom
|
||||
>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-btn icon v-on="on">
|
||||
<v-icon>mdi-account-circle</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<v-card
|
||||
class="mx-auto"
|
||||
max-width="344"
|
||||
outlined
|
||||
>
|
||||
<v-list-item three-line>
|
||||
<v-list-item-content>
|
||||
<div class="overline mb-4">connected as</div>
|
||||
<v-list-item-title class="headline mb-1">{{user.name}}</v-list-item-title>
|
||||
<v-list-item-subtitle>Email: {{user.email}}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>Issuer: {{user.issuer}}</v-list-item-subtitle>
|
||||
<v-list-item-subtitle>Issued at: {{ user.issuedAt | formatDate }}</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-card-actions>
|
||||
<v-btn small
|
||||
v-on:click="logout()"
|
||||
>
|
||||
logout
|
||||
<v-icon small right dark>mdi-logout</v-icon>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
|
||||
</v-app-bar>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {mapActions, mapGetters} from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'Header',
|
||||
|
||||
computed:{
|
||||
...mapGetters({
|
||||
user: 'auth/user',
|
||||
}),
|
||||
},
|
||||
|
||||
methods: {
|
||||
...mapActions('auth', {
|
||||
logout: 'logout',
|
||||
}),
|
||||
}
|
||||
}
|
||||
</script>
|
@ -158,7 +158,7 @@
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
color="success"
|
||||
:href="`${apiBaseUrl}/server/config`"
|
||||
v-on:click="forceFileDownload()"
|
||||
>
|
||||
Download server configuration
|
||||
<v-icon right dark>mdi-cloud-download-outline</v-icon>
|
||||
@ -167,52 +167,44 @@
|
||||
<v-btn
|
||||
class="ma-2"
|
||||
color="warning"
|
||||
@click="updateServer"
|
||||
@click="update"
|
||||
>
|
||||
Update server configuration
|
||||
<v-icon right dark>mdi-update</v-icon>
|
||||
</v-btn>
|
||||
<v-divider dark/>
|
||||
</v-row>
|
||||
<Notification v-bind:notification="notification"/>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import {API_BASE_URL, ApiService} from "../services/ApiService";
|
||||
import Notification from '../components/Notification'
|
||||
import {mapActions, mapGetters} from "vuex";
|
||||
|
||||
export default {
|
||||
name: 'Server',
|
||||
|
||||
components: {
|
||||
Notification
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
api: null,
|
||||
server: null,
|
||||
apiBaseUrl: API_BASE_URL,
|
||||
notification: {
|
||||
show: false,
|
||||
color: '',
|
||||
text: '',
|
||||
},
|
||||
|
||||
}),
|
||||
|
||||
computed:{
|
||||
...mapGetters({
|
||||
server: 'server/server',
|
||||
config: 'server/config',
|
||||
}),
|
||||
},
|
||||
|
||||
mounted () {
|
||||
this.api = new ApiService();
|
||||
this.getServer()
|
||||
this.readServer()
|
||||
},
|
||||
|
||||
methods: {
|
||||
getServer() {
|
||||
this.api.get('/server').then((res) => {
|
||||
this.server = res;
|
||||
}).catch((e) => {
|
||||
this.notify('error', e.response.status + ' ' + e.response.statusText);
|
||||
});
|
||||
},
|
||||
updateServer () {
|
||||
...mapActions('server', {
|
||||
errorServer: 'error',
|
||||
readServer: 'read',
|
||||
updateServer: 'update',
|
||||
}),
|
||||
|
||||
update() {
|
||||
// convert int values
|
||||
this.server.listenPort = parseInt(this.server.listenPort, 10);
|
||||
this.server.persistentKeepalive = parseInt(this.server.persistentKeepalive, 10);
|
||||
@ -220,12 +212,12 @@
|
||||
|
||||
// check server addresses
|
||||
if (this.server.address.length < 1) {
|
||||
this.notify('error', 'Please provide at least one valid CIDR address for server interface');
|
||||
this.errorServer('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`);
|
||||
this.errorServer(`Invalid CIDR detected, please correct ${this.server.address[i]} before submitting`);
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -233,35 +225,34 @@
|
||||
// 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`);
|
||||
this.errorServer(`Invalid IP detected, please correct ${this.server.dns[i]} before submitting`);
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// check client AllowedIPs
|
||||
if (this.server.allowedips.length < 1) {
|
||||
this.notify('error', 'Please provide at least one valid CIDR address for client allowed IPs');
|
||||
this.errorServer('Please provide at least one valid CIDR address for client allowed IPs');
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < this.server.allowedips.length; i++){
|
||||
if (this.$isCidr(this.server.allowedips[i]) === 0) {
|
||||
this.notify('error', 'Invalid CIDR detected, please correct before submitting');
|
||||
this.errorServer('Invalid CIDR detected, please correct 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);
|
||||
});
|
||||
this.updateServer(this.server)
|
||||
},
|
||||
|
||||
forceFileDownload(){
|
||||
const url = window.URL.createObjectURL(new Blob([this.config]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', 'wg0.conf') //or any other extension
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
},
|
||||
notify(color, msg) {
|
||||
this.notification.show = true;
|
||||
this.notification.color = color;
|
||||
this.notification.text = msg;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -1,14 +1,18 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import vuetify from './plugins/vuetify';
|
||||
import './plugins/moment';
|
||||
import './plugins/cidr'
|
||||
import './plugins/axios'
|
||||
|
||||
Vue.config.productionTip = false
|
||||
// Don't warn about using the dev version of Vue in development.
|
||||
Vue.config.productionTip = process.env.NODE_ENV === 'production'
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
store,
|
||||
vuetify,
|
||||
render: function (h) { return h(App) }
|
||||
}).$mount('#app')
|
||||
|
26
ui/src/plugins/axios.js
Normal file
26
ui/src/plugins/axios.js
Normal file
@ -0,0 +1,26 @@
|
||||
import Vue from 'vue'
|
||||
import axios from "axios";
|
||||
import VueAxios from "vue-axios";
|
||||
import TokenService from "../services/token.service";
|
||||
|
||||
Vue.use(VueAxios, axios);
|
||||
|
||||
let baseUrl = "/api/v1.0";
|
||||
if (process.env.NODE_ENV === "development"){
|
||||
baseUrl = process.env.VUE_APP_API_BASE_URL;
|
||||
}
|
||||
|
||||
Vue.axios.defaults.baseURL = baseUrl;
|
||||
|
||||
Vue.axios.interceptors.response.use(function (response) {
|
||||
return response;
|
||||
}, function (error) {
|
||||
if (401 === error.response.status) {
|
||||
TokenService.destroyToken();
|
||||
TokenService.destroyClientId();
|
||||
window.location = '/';
|
||||
} else {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
});
|
||||
|
@ -1,22 +1,19 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import store from "../store";
|
||||
|
||||
Vue.use(VueRouter);
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'index',
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "Index" */ '../views/Index.vue')
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/clients',
|
||||
name: 'clients',
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "Clients" */ '../views/Clients.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/server',
|
||||
@ -24,6 +21,9 @@ const routes = [
|
||||
component: function () {
|
||||
return import(/* webpackChunkName: "Server" */ '../views/Server.vue')
|
||||
},
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
@ -33,4 +33,16 @@ const router = new VueRouter({
|
||||
routes
|
||||
});
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
if(to.matched.some(record => record.meta.requiresAuth)) {
|
||||
if (store.getters["auth/isAuthenticated"]) {
|
||||
next()
|
||||
return
|
||||
}
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
@ -1,40 +0,0 @@
|
||||
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)
|
||||
};
|
||||
}
|
59
ui/src/services/api.service.js
Normal file
59
ui/src/services/api.service.js
Normal file
@ -0,0 +1,59 @@
|
||||
import Vue from "vue";
|
||||
import TokenService from "./token.service";
|
||||
|
||||
const ApiService = {
|
||||
|
||||
setHeader() {
|
||||
Vue.axios.defaults.headers.common.Authorization = `${TokenService.getToken()}`;
|
||||
},
|
||||
|
||||
get(resource) {
|
||||
return Vue.axios.get(resource)
|
||||
.then(response => response.data)
|
||||
.catch(error => {
|
||||
throw new Error(`ApiService: ${error}`)
|
||||
});
|
||||
},
|
||||
|
||||
post(resource, params) {
|
||||
return Vue.axios.post(resource, params)
|
||||
.then(response => response.data)
|
||||
.catch(error => {
|
||||
throw new Error(`ApiService: ${error}`)
|
||||
});
|
||||
},
|
||||
|
||||
put(resource, params) {
|
||||
return Vue.axios.put(resource, params)
|
||||
.then(response => response.data)
|
||||
.catch(error => {
|
||||
throw new Error(`ApiService: ${error}`)
|
||||
});
|
||||
},
|
||||
|
||||
patch(resource, params) {
|
||||
return Vue.axios.patch(resource, params)
|
||||
.then(response => response.data)
|
||||
.catch(error => {
|
||||
throw new Error(`ApiService: ${error}`)
|
||||
});
|
||||
},
|
||||
|
||||
delete(resource) {
|
||||
return Vue.axios.delete(resource)
|
||||
.then(response => response.data)
|
||||
.catch(error => {
|
||||
throw new Error(`ApiService: ${error}`)
|
||||
});
|
||||
},
|
||||
|
||||
getWithConfig(resource, config) {
|
||||
return Vue.axios.get(resource, config)
|
||||
.then(response => response.data)
|
||||
.catch(error => {
|
||||
throw new Error(`ApiService: ${error}`)
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default ApiService;
|
35
ui/src/services/token.service.js
Normal file
35
ui/src/services/token.service.js
Normal file
@ -0,0 +1,35 @@
|
||||
const TOKEN_KEY = "token";
|
||||
const CLIENT_ID_KEY = "client_id";
|
||||
|
||||
export const getToken = () => {
|
||||
return window.localStorage.getItem(TOKEN_KEY);
|
||||
};
|
||||
|
||||
export const saveToken = token => {
|
||||
window.localStorage.setItem(TOKEN_KEY, token);
|
||||
};
|
||||
|
||||
export const destroyToken = () => {
|
||||
window.localStorage.removeItem(TOKEN_KEY);
|
||||
};
|
||||
|
||||
export const getClientId = () => {
|
||||
return window.localStorage.getItem(CLIENT_ID_KEY);
|
||||
};
|
||||
|
||||
export const saveClientId = token => {
|
||||
window.localStorage.setItem(CLIENT_ID_KEY, token);
|
||||
};
|
||||
|
||||
export const destroyClientId = () => {
|
||||
window.localStorage.removeItem(CLIENT_ID_KEY);
|
||||
};
|
||||
|
||||
export default {
|
||||
getToken,
|
||||
saveToken,
|
||||
destroyToken,
|
||||
getClientId,
|
||||
saveClientId,
|
||||
destroyClientId
|
||||
};
|
19
ui/src/store/index.js
Normal file
19
ui/src/store/index.js
Normal file
@ -0,0 +1,19 @@
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import auth from "./modules/auth";
|
||||
import client from "./modules/client";
|
||||
import server from "./modules/server";
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: {},
|
||||
getters : {},
|
||||
mutations: {},
|
||||
actions:{},
|
||||
modules: {
|
||||
auth,
|
||||
client,
|
||||
server
|
||||
}
|
||||
})
|
126
ui/src/store/modules/auth.js
Normal file
126
ui/src/store/modules/auth.js
Normal file
@ -0,0 +1,126 @@
|
||||
import ApiService from "../../services/api.service";
|
||||
import TokenService from "../../services/token.service";
|
||||
|
||||
const state = {
|
||||
error: null,
|
||||
user: null,
|
||||
authStatus: '',
|
||||
authRedirectUrl: '',
|
||||
};
|
||||
|
||||
const getters = {
|
||||
error(state) {
|
||||
return state.error;
|
||||
},
|
||||
user(state) {
|
||||
return state.user;
|
||||
},
|
||||
isAuthenticated(state) {
|
||||
return state.user !== null;
|
||||
},
|
||||
authRedirectUrl(state) {
|
||||
return state.authRedirectUrl
|
||||
},
|
||||
authStatus(state) {
|
||||
return state.authStatus
|
||||
},
|
||||
};
|
||||
|
||||
const actions = {
|
||||
user({ commit }){
|
||||
ApiService.get("/auth/user")
|
||||
.then( resp => {
|
||||
commit('user', resp)
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err);
|
||||
commit('logout')
|
||||
});
|
||||
},
|
||||
|
||||
oauth2_url({ commit, dispatch }){
|
||||
if (TokenService.getToken()) {
|
||||
ApiService.setHeader();
|
||||
dispatch('user');
|
||||
return
|
||||
}
|
||||
ApiService.get("/auth/oauth2_url")
|
||||
.then(resp => {
|
||||
if (resp.codeUrl === '_magic_string_fake_auth_no_redirect_'){
|
||||
console.log("server report oauth2 is disabled, fake exchange")
|
||||
commit('authStatus', 'disabled')
|
||||
TokenService.saveClientId(resp.clientId)
|
||||
dispatch('oauth2_exchange', {code: "", state: resp.state})
|
||||
} else {
|
||||
commit('authStatus', 'redirect')
|
||||
commit('authRedirectUrl', resp)
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
commit('authStatus', 'error')
|
||||
commit('error', err);
|
||||
commit('logout')
|
||||
})
|
||||
},
|
||||
|
||||
oauth2_exchange({ commit, dispatch }, data){
|
||||
data.clientId = TokenService.getClientId()
|
||||
ApiService.post("/auth/oauth2_exchange", data)
|
||||
.then(resp => {
|
||||
commit('authStatus', 'success')
|
||||
commit('token', resp)
|
||||
dispatch('user');
|
||||
})
|
||||
.catch(err => {
|
||||
commit('authStatus', 'error')
|
||||
commit('error', err);
|
||||
commit('logout')
|
||||
})
|
||||
},
|
||||
|
||||
logout({ commit }){
|
||||
ApiService.get("/auth/logout")
|
||||
.then(resp => {
|
||||
commit('logout')
|
||||
})
|
||||
.catch(err => {
|
||||
commit('authStatus', '')
|
||||
commit('error', err);
|
||||
commit('logout')
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
error(state, error) {
|
||||
state.error = error;
|
||||
},
|
||||
authStatus(state, authStatus) {
|
||||
state.authStatus = authStatus;
|
||||
},
|
||||
authRedirectUrl(state, resp) {
|
||||
state.authRedirectUrl = resp.codeUrl;
|
||||
TokenService.saveClientId(resp.clientId);
|
||||
},
|
||||
token(state, token) {
|
||||
TokenService.saveToken(token);
|
||||
ApiService.setHeader();
|
||||
TokenService.destroyClientId();
|
||||
},
|
||||
user(state, user) {
|
||||
state.user = user;
|
||||
},
|
||||
logout(state) {
|
||||
state.user = null;
|
||||
TokenService.destroyToken();
|
||||
TokenService.destroyClientId();
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
177
ui/src/store/modules/client.js
Normal file
177
ui/src/store/modules/client.js
Normal file
@ -0,0 +1,177 @@
|
||||
import ApiService from "../../services/api.service";
|
||||
|
||||
const state = {
|
||||
error: null,
|
||||
clients: [],
|
||||
clientQrcodes: [],
|
||||
clientConfigs: []
|
||||
}
|
||||
|
||||
const getters = {
|
||||
error(state) {
|
||||
return state.error;
|
||||
},
|
||||
clients(state) {
|
||||
return state.clients;
|
||||
},
|
||||
getClientQrcode: (state) => (id) => {
|
||||
let item = state.clientQrcodes.find(item => item.id === id)
|
||||
// initial load fails, must wait promise and stuff...
|
||||
return item ? item.qrcode : "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
||||
},
|
||||
getClientConfig: (state) => (id) => {
|
||||
let item = state.clientConfigs.find(item => item.id === id)
|
||||
return item ? item.config : null
|
||||
}
|
||||
}
|
||||
|
||||
const actions = {
|
||||
error({ commit }, error){
|
||||
commit('error', error)
|
||||
},
|
||||
|
||||
readAll({ commit, dispatch }){
|
||||
ApiService.get("/client")
|
||||
.then(resp => {
|
||||
commit('clients', resp)
|
||||
dispatch('readQrcodes')
|
||||
dispatch('readConfigs')
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
create({ commit, dispatch }, client){
|
||||
ApiService.post("/client", client)
|
||||
.then(resp => {
|
||||
dispatch('readQrcode', resp)
|
||||
dispatch('readConfig', resp)
|
||||
commit('create', resp)
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
update({ commit, dispatch }, client){
|
||||
ApiService.patch(`/client/${client.id}`, client)
|
||||
.then(resp => {
|
||||
dispatch('readQrcode', resp)
|
||||
dispatch('readConfig', resp)
|
||||
commit('update', resp)
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
delete({ commit }, client){
|
||||
ApiService.delete(`/client/${client.id}`)
|
||||
.then(() => {
|
||||
commit('delete', client)
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
email({ commit }, client){
|
||||
ApiService.get(`/client/${client.id}/email`)
|
||||
.then(() => {
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
readQrcode({ state, commit }, client){
|
||||
ApiService.getWithConfig(`/client/${client.id}/config?qrcode=true`, {responseType: 'arraybuffer'})
|
||||
.then(resp => {
|
||||
let image = Buffer.from(resp, 'binary').toString('base64')
|
||||
commit('clientQrcodes', { client, image })
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
readConfig({ state, commit }, client){
|
||||
ApiService.getWithConfig(`/client/${client.id}/config?qrcode=false`, {responseType: 'arraybuffer'})
|
||||
.then(resp => {
|
||||
commit('clientConfigs', { client: client, config: resp })
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
readQrcodes({ state, dispatch }){
|
||||
state.clients.forEach(client => {
|
||||
dispatch('readQrcode', client)
|
||||
})
|
||||
},
|
||||
|
||||
readConfigs({ state, dispatch }){
|
||||
state.clients.forEach(client => {
|
||||
dispatch('readConfig', client)
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
error(state, error) {
|
||||
state.error = error;
|
||||
},
|
||||
clients(state, clients){
|
||||
state.clients = clients
|
||||
},
|
||||
create(state, client){
|
||||
state.clients.push(client)
|
||||
},
|
||||
update(state, client){
|
||||
let index = state.clients.findIndex(x => x.id === client.id);
|
||||
if (index !== -1) {
|
||||
state.clients.splice(index, 1);
|
||||
state.clients.push(client);
|
||||
} else {
|
||||
state.error = "update client failed, not in list"
|
||||
}
|
||||
},
|
||||
delete(state, client){
|
||||
let index = state.clients.findIndex(x => x.id === client.id);
|
||||
if (index !== -1) {
|
||||
state.clients.splice(index, 1);
|
||||
} else {
|
||||
state.error = "delete client failed, not in list"
|
||||
}
|
||||
},
|
||||
clientQrcodes(state, { client, image }){
|
||||
let index = state.clientQrcodes.findIndex(x => x.id === client.id);
|
||||
if (index !== -1) {
|
||||
state.clientQrcodes.splice(index, 1);
|
||||
}
|
||||
state.clientQrcodes.push({
|
||||
id: client.id,
|
||||
qrcode: image
|
||||
})
|
||||
},
|
||||
clientConfigs(state, { client, config }){
|
||||
let index = state.clientConfigs.findIndex(x => x.id === client.id);
|
||||
if (index !== -1) {
|
||||
state.clientConfigs.splice(index, 1);
|
||||
}
|
||||
state.clientConfigs.push({
|
||||
id: client.id,
|
||||
config: config
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
100
ui/src/store/modules/server.js
Normal file
100
ui/src/store/modules/server.js
Normal file
@ -0,0 +1,100 @@
|
||||
import ApiService from "../../services/api.service";
|
||||
|
||||
const state = {
|
||||
error: null,
|
||||
server: null,
|
||||
config: '',
|
||||
version: '_ci_build_not_run_properly_',
|
||||
}
|
||||
|
||||
const getters = {
|
||||
error(state) {
|
||||
return state.error;
|
||||
},
|
||||
|
||||
server(state) {
|
||||
return state.server;
|
||||
},
|
||||
|
||||
version(state) {
|
||||
return state.version;
|
||||
},
|
||||
|
||||
config(state) {
|
||||
return state.config;
|
||||
},
|
||||
}
|
||||
|
||||
const actions = {
|
||||
error({ commit }, error){
|
||||
commit('error', error)
|
||||
},
|
||||
|
||||
read({ commit, dispatch }){
|
||||
ApiService.get("/server")
|
||||
.then(resp => {
|
||||
commit('server', resp)
|
||||
dispatch('config')
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
update({ commit }, server){
|
||||
ApiService.patch(`/server`, server)
|
||||
.then(resp => {
|
||||
commit('server', resp)
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
config({ commit }){
|
||||
ApiService.getWithConfig("/server/config", {responseType: 'arraybuffer'})
|
||||
.then(resp => {
|
||||
commit('config', resp)
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
version({ commit }){
|
||||
ApiService.get("/server/version")
|
||||
.then(resp => {
|
||||
commit('version', resp.version)
|
||||
})
|
||||
.catch(err => {
|
||||
commit('error', err)
|
||||
})
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
error(state, error) {
|
||||
state.error = error;
|
||||
},
|
||||
|
||||
server(state, server){
|
||||
state.server = server
|
||||
},
|
||||
|
||||
config(state, config){
|
||||
state.config = config
|
||||
},
|
||||
|
||||
version(state, version){
|
||||
state.version = version
|
||||
},
|
||||
}
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state,
|
||||
getters,
|
||||
actions,
|
||||
mutations
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
created () {
|
||||
this.$router.replace({ name: 'clients' })
|
||||
}
|
||||
}
|
||||
</script>
|
30
util/util.go
30
util/util.go
@ -1,6 +1,8 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
@ -9,6 +11,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
AuthTokenHeaderName = "Authorization"
|
||||
// RegexpEmail check valid email
|
||||
RegexpEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
)
|
||||
@ -51,7 +54,7 @@ func DirectoryExists(name string) bool {
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
// GetAvailableIp search for an available in cidr against a list of reserved ips
|
||||
// GetAvailableIp search for an available ip in cidr against a list of reserved ips
|
||||
func GetAvailableIp(cidr string, reserved []string) (string, error) {
|
||||
ip, ipnet, err := net.ParseCIDR(cidr)
|
||||
if err != nil {
|
||||
@ -132,3 +135,28 @@ func BroadcastAddr(n *net.IPNet) net.IP {
|
||||
}
|
||||
return broadcast
|
||||
}
|
||||
|
||||
// GenerateRandomBytes returns securely generated random bytes.
|
||||
// It will return an error if the system's secure random
|
||||
// number generator fails to function correctly, in which
|
||||
// case the caller should not continue.
|
||||
func GenerateRandomBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
// Note that err == nil only if we read len(b) bytes.
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// GenerateRandomString returns a URL-safe, base64 encoded
|
||||
// securely generated random string.
|
||||
// It will return an error if the system's secure random
|
||||
// number generator fails to function correctly, in which
|
||||
// case the caller should not continue.
|
||||
func GenerateRandomString(s int) (string, error) {
|
||||
b, err := GenerateRandomBytes(s)
|
||||
return base64.URLEncoding.EncodeToString(b), err
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user