mirror of
				https://github.com/h44z/wg-portal.git
				synced 2025-11-04 08:06:18 +00:00 
			
		
		
		
	RESTful API for WireGuard Portal (#11)
This commit is contained in:
		@@ -114,6 +114,7 @@ The following configuration options are available:
 | 
				
			|||||||
| ADMIN_PASS            | adminPass         | core        | wgportal                                        | The administrator password. If unchanged, a random password will be set on first startup.                                            |
 | 
					| ADMIN_PASS            | adminPass         | core        | wgportal                                        | The administrator password. If unchanged, a random password will be set on first startup.                                            |
 | 
				
			||||||
| EDITABLE_KEYS         | editableKeys      | core        | true                                            | Allow to edit key-pairs in the UI.                                                                                                   |
 | 
					| EDITABLE_KEYS         | editableKeys      | core        | true                                            | Allow to edit key-pairs in the UI.                                                                                                   |
 | 
				
			||||||
| CREATE_DEFAULT_PEER   | createDefaultPeer | core        | false                                           | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. |
 | 
					| CREATE_DEFAULT_PEER   | createDefaultPeer | core        | false                                           | If an LDAP user logs in for the first time, a new WireGuard peer will be created on the WG_DEFAULT_DEVICE if this option is enabled. |
 | 
				
			||||||
 | 
					| SELF_PROVISIONING     | selfProvisioning  | core        | false                                           | Allow registered users to automatically create peers via the RESTful API.                                                            |
 | 
				
			||||||
| LDAP_ENABLED          | ldapEnabled       | core        | false                                           | Enable or disable the LDAP backend.                                                                                                  |
 | 
					| LDAP_ENABLED          | ldapEnabled       | core        | false                                           | Enable or disable the LDAP backend.                                                                                                  |
 | 
				
			||||||
| SESSION_SECRET        | sessionSecret     | core        | secret                                          | Use a custom secret to encrypt session data.                                                                                         |
 | 
					| SESSION_SECRET        | sessionSecret     | core        | secret                                          | Use a custom secret to encrypt session data.                                                                                         |
 | 
				
			||||||
| DATABASE_TYPE         | typ               | database    | sqlite                                          | Either mysql or sqlite.                                                                                                              |
 | 
					| DATABASE_TYPE         | typ               | database    | sqlite                                          | Either mysql or sqlite.                                                                                                              |
 | 
				
			||||||
@@ -191,6 +192,11 @@ wg:
 | 
				
			|||||||
  manageIPAddresses: true
 | 
					  manageIPAddresses: true
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### RESTful API
 | 
				
			||||||
 | 
					WireGuard Portal offers a RESTful API to interact with. 
 | 
				
			||||||
 | 
					The API is documented using OpenAPI 2.0, the Swagger UI can be found 
 | 
				
			||||||
 | 
					under the URL `http://<your wg-portal ip/domain>/swagger/index.html`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## What is out of scope
 | 
					## What is out of scope
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 * Generation or application of any `iptables` or `nftables` rules
 | 
					 * Generation or application of any `iptables` or `nftables` rules
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -136,7 +136,7 @@ func (provider Provider) InitializeAdmin(email, password string) error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		admin.Email = email
 | 
							admin.Email = email
 | 
				
			||||||
		admin.Password = string(hashedPassword)
 | 
							admin.Password = users.PrivateString(hashedPassword)
 | 
				
			||||||
		admin.Firstname = "WireGuard"
 | 
							admin.Firstname = "WireGuard"
 | 
				
			||||||
		admin.Lastname = "Administrator"
 | 
							admin.Lastname = "Administrator"
 | 
				
			||||||
		admin.CreatedAt = time.Now()
 | 
							admin.CreatedAt = time.Now()
 | 
				
			||||||
@@ -170,7 +170,7 @@ func (provider Provider) InitializeAdmin(email, password string) error {
 | 
				
			|||||||
			return errors.Wrap(err, "failed to hash admin password")
 | 
								return errors.Wrap(err, "failed to hash admin password")
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		admin.Password = string(hashedPassword)
 | 
							admin.Password = users.PrivateString(hashedPassword)
 | 
				
			||||||
		admin.IsAdmin = true
 | 
							admin.IsAdmin = true
 | 
				
			||||||
		admin.UpdatedAt = time.Now()
 | 
							admin.UpdatedAt = time.Now()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,10 +7,13 @@ import (
 | 
				
			|||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	jsonpatch "github.com/evanphx/json-patch"
 | 
						jsonpatch "github.com/evanphx/json-patch"
 | 
				
			||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
 | 
						"github.com/h44z/wg-portal/internal/common"
 | 
				
			||||||
	"github.com/h44z/wg-portal/internal/users"
 | 
						"github.com/h44z/wg-portal/internal/users"
 | 
				
			||||||
 | 
						"github.com/h44z/wg-portal/internal/wireguard"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @title WireGuard Portal API
 | 
					// @title WireGuard Portal API
 | 
				
			||||||
@@ -20,9 +23,18 @@ import (
 | 
				
			|||||||
// @license.name MIT
 | 
					// @license.name MIT
 | 
				
			||||||
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
 | 
					// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @contact.name WireGuard Portal Project
 | 
				
			||||||
 | 
					// @contact.url https://github.com/h44z/wg-portal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @securityDefinitions.basic ApiBasicAuth
 | 
					// @securityDefinitions.basic ApiBasicAuth
 | 
				
			||||||
// @in header
 | 
					// @in header
 | 
				
			||||||
// @name Authorization
 | 
					// @name Authorization
 | 
				
			||||||
 | 
					// @scope.admin Admin access required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// @securityDefinitions.basic GeneralBasicAuth
 | 
				
			||||||
 | 
					// @in header
 | 
				
			||||||
 | 
					// @name Authorization
 | 
				
			||||||
 | 
					// @scope.user User access required
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// @BasePath /api/v1
 | 
					// @BasePath /api/v1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -36,24 +48,23 @@ type ApiError struct {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetUsers godoc
 | 
					// GetUsers godoc
 | 
				
			||||||
 | 
					// @Tags Users
 | 
				
			||||||
// @Summary Retrieves all users
 | 
					// @Summary Retrieves all users
 | 
				
			||||||
// @Produce json
 | 
					// @Produce json
 | 
				
			||||||
// @Success 200 {object} []users.User
 | 
					// @Success 200 {object} []users.User
 | 
				
			||||||
// @Failure 401 {object} ApiError
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
// @Failure 403 {object} ApiError
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
// @Failure 404 {object} ApiError
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
// @Router /users [get]
 | 
					// @Router /backend/users [get]
 | 
				
			||||||
// @Security ApiBasicAuth
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
func (s *ApiServer) GetUsers(c *gin.Context) {
 | 
					func (s *ApiServer) GetUsers(c *gin.Context) {
 | 
				
			||||||
	allUsers := s.s.users.GetUsersUnscoped()
 | 
						allUsers := s.s.users.GetUsersUnscoped()
 | 
				
			||||||
	for i := range allUsers {
 | 
					 | 
				
			||||||
		allUsers[i].Password = "" // do not publish password...
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	c.JSON(http.StatusOK, allUsers)
 | 
						c.JSON(http.StatusOK, allUsers)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// GetUser godoc
 | 
					// GetUser godoc
 | 
				
			||||||
 | 
					// @Tags Users
 | 
				
			||||||
// @Summary Retrieves user based on given Email
 | 
					// @Summary Retrieves user based on given Email
 | 
				
			||||||
// @Produce json
 | 
					// @Produce json
 | 
				
			||||||
// @Param email path string true "User Email"
 | 
					// @Param email path string true "User Email"
 | 
				
			||||||
@@ -62,7 +73,7 @@ func (s *ApiServer) GetUsers(c *gin.Context) {
 | 
				
			|||||||
// @Failure 401 {object} ApiError
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
// @Failure 403 {object} ApiError
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
// @Failure 404 {object} ApiError
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
// @Router /user/{email} [get]
 | 
					// @Router /backend/user/{email} [get]
 | 
				
			||||||
// @Security ApiBasicAuth
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
func (s *ApiServer) GetUser(c *gin.Context) {
 | 
					func (s *ApiServer) GetUser(c *gin.Context) {
 | 
				
			||||||
	email := strings.ToLower(strings.TrimSpace(c.Param("email")))
 | 
						email := strings.ToLower(strings.TrimSpace(c.Param("email")))
 | 
				
			||||||
@@ -76,20 +87,22 @@ func (s *ApiServer) GetUser(c *gin.Context) {
 | 
				
			|||||||
		c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	user.Password = "" // do not send password...
 | 
					 | 
				
			||||||
	c.JSON(http.StatusOK, user)
 | 
						c.JSON(http.StatusOK, user)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PostUser godoc
 | 
					// PostUser godoc
 | 
				
			||||||
 | 
					// @Tags Users
 | 
				
			||||||
// @Summary Creates a new user based on the given user model
 | 
					// @Summary Creates a new user based on the given user model
 | 
				
			||||||
 | 
					// @Accept  json
 | 
				
			||||||
// @Produce json
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param user body users.User true "User Model"
 | 
				
			||||||
// @Success 200 {object} users.User
 | 
					// @Success 200 {object} users.User
 | 
				
			||||||
// @Failure 400 {object} ApiError
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
// @Failure 401 {object} ApiError
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
// @Failure 403 {object} ApiError
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
// @Failure 404 {object} ApiError
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
// @Failure 500 {object} ApiError
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
// @Router /users [post]
 | 
					// @Router /backend/users [post]
 | 
				
			||||||
// @Security ApiBasicAuth
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
func (s *ApiServer) PostUser(c *gin.Context) {
 | 
					func (s *ApiServer) PostUser(c *gin.Context) {
 | 
				
			||||||
	newUser := users.User{}
 | 
						newUser := users.User{}
 | 
				
			||||||
@@ -113,21 +126,23 @@ func (s *ApiServer) PostUser(c *gin.Context) {
 | 
				
			|||||||
		c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	user.Password = "" // do not send password...
 | 
					 | 
				
			||||||
	c.JSON(http.StatusOK, user)
 | 
						c.JSON(http.StatusOK, user)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PutUser godoc
 | 
					// PutUser godoc
 | 
				
			||||||
 | 
					// @Tags Users
 | 
				
			||||||
// @Summary Updates a user based on the given user model
 | 
					// @Summary Updates a user based on the given user model
 | 
				
			||||||
 | 
					// @Accept  json
 | 
				
			||||||
// @Produce json
 | 
					// @Produce json
 | 
				
			||||||
// @Param email path string true "User Email"
 | 
					// @Param email path string true "User Email"
 | 
				
			||||||
 | 
					// @Param user body users.User true "User Model"
 | 
				
			||||||
// @Success 200 {object} users.User
 | 
					// @Success 200 {object} users.User
 | 
				
			||||||
// @Failure 400 {object} ApiError
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
// @Failure 401 {object} ApiError
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
// @Failure 403 {object} ApiError
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
// @Failure 404 {object} ApiError
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
// @Failure 500 {object} ApiError
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
// @Router /user/{email} [put]
 | 
					// @Router /backend/user/{email} [put]
 | 
				
			||||||
// @Security ApiBasicAuth
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
func (s *ApiServer) PutUser(c *gin.Context) {
 | 
					func (s *ApiServer) PutUser(c *gin.Context) {
 | 
				
			||||||
	email := strings.ToLower(strings.TrimSpace(c.Param("email")))
 | 
						email := strings.ToLower(strings.TrimSpace(c.Param("email")))
 | 
				
			||||||
@@ -163,21 +178,23 @@ func (s *ApiServer) PutUser(c *gin.Context) {
 | 
				
			|||||||
		c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	user.Password = "" // do not send password...
 | 
					 | 
				
			||||||
	c.JSON(http.StatusOK, user)
 | 
						c.JSON(http.StatusOK, user)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// PatchUser godoc
 | 
					// PatchUser godoc
 | 
				
			||||||
 | 
					// @Tags Users
 | 
				
			||||||
// @Summary Updates a user based on the given partial user model
 | 
					// @Summary Updates a user based on the given partial user model
 | 
				
			||||||
 | 
					// @Accept  json
 | 
				
			||||||
// @Produce json
 | 
					// @Produce json
 | 
				
			||||||
// @Param email path string true "User Email"
 | 
					// @Param email path string true "User Email"
 | 
				
			||||||
 | 
					// @Param user body users.User true "User Model"
 | 
				
			||||||
// @Success 200 {object} users.User
 | 
					// @Success 200 {object} users.User
 | 
				
			||||||
// @Failure 400 {object} ApiError
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
// @Failure 401 {object} ApiError
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
// @Failure 403 {object} ApiError
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
// @Failure 404 {object} ApiError
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
// @Failure 500 {object} ApiError
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
// @Router /user/{email} [patch]
 | 
					// @Router /backend/user/{email} [patch]
 | 
				
			||||||
// @Security ApiBasicAuth
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
func (s *ApiServer) PatchUser(c *gin.Context) {
 | 
					func (s *ApiServer) PatchUser(c *gin.Context) {
 | 
				
			||||||
	email := strings.ToLower(strings.TrimSpace(c.Param("email")))
 | 
						email := strings.ToLower(strings.TrimSpace(c.Param("email")))
 | 
				
			||||||
@@ -227,11 +244,11 @@ func (s *ApiServer) PatchUser(c *gin.Context) {
 | 
				
			|||||||
		c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | 
				
			||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	user.Password = "" // do not send password...
 | 
					 | 
				
			||||||
	c.JSON(http.StatusOK, user)
 | 
						c.JSON(http.StatusOK, user)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// DeleteUser godoc
 | 
					// DeleteUser godoc
 | 
				
			||||||
 | 
					// @Tags Users
 | 
				
			||||||
// @Summary Deletes the specified user
 | 
					// @Summary Deletes the specified user
 | 
				
			||||||
// @Produce json
 | 
					// @Produce json
 | 
				
			||||||
// @Param email path string true "User Email"
 | 
					// @Param email path string true "User Email"
 | 
				
			||||||
@@ -241,7 +258,7 @@ func (s *ApiServer) PatchUser(c *gin.Context) {
 | 
				
			|||||||
// @Failure 403 {object} ApiError
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
// @Failure 404 {object} ApiError
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
// @Failure 500 {object} ApiError
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
// @Router /user/{email} [delete]
 | 
					// @Router /backend/user/{email} [delete]
 | 
				
			||||||
// @Security ApiBasicAuth
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
func (s *ApiServer) DeleteUser(c *gin.Context) {
 | 
					func (s *ApiServer) DeleteUser(c *gin.Context) {
 | 
				
			||||||
	email := strings.ToLower(strings.TrimSpace(c.Param("email")))
 | 
						email := strings.ToLower(strings.TrimSpace(c.Param("email")))
 | 
				
			||||||
@@ -263,3 +280,590 @@ func (s *ApiServer) DeleteUser(c *gin.Context) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	c.Status(http.StatusNoContent)
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPeers godoc
 | 
				
			||||||
 | 
					// @Tags Peers
 | 
				
			||||||
 | 
					// @Summary Retrieves all peers for the given interface
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param device path string true "Device Name"
 | 
				
			||||||
 | 
					// @Success 200 {object} []wireguard.Peer
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/peers/{device} [get]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) GetPeers(c *gin.Context) {
 | 
				
			||||||
 | 
						deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
 | 
				
			||||||
 | 
						if deviceName == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// validate device name
 | 
				
			||||||
 | 
						if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peers := s.s.peers.GetAllPeers(deviceName)
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, peers)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPeer godoc
 | 
				
			||||||
 | 
					// @Tags Peers
 | 
				
			||||||
 | 
					// @Summary Retrieves the peer for the given public key
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param pkey path string true "Public Key (Base 64)"
 | 
				
			||||||
 | 
					// @Success 200 {object} wireguard.Peer
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/peer/{pkey} [get]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) GetPeer(c *gin.Context) {
 | 
				
			||||||
 | 
						pkey := c.Param("pkey")
 | 
				
			||||||
 | 
						if pkey == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peer := s.s.peers.GetPeerByKey(pkey)
 | 
				
			||||||
 | 
						if !peer.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, peer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PostPeer godoc
 | 
				
			||||||
 | 
					// @Tags Peers
 | 
				
			||||||
 | 
					// @Summary Creates a new peer based on the given peer model
 | 
				
			||||||
 | 
					// @Accept  json
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param device path string true "Device Name"
 | 
				
			||||||
 | 
					// @Param peer body wireguard.Peer true "Peer Model"
 | 
				
			||||||
 | 
					// @Success 200 {object} wireguard.Peer
 | 
				
			||||||
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/peers/{device} [post]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) PostPeer(c *gin.Context) {
 | 
				
			||||||
 | 
						deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
 | 
				
			||||||
 | 
						if deviceName == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// validate device name
 | 
				
			||||||
 | 
						if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						newPeer := wireguard.Peer{}
 | 
				
			||||||
 | 
						if err := c.BindJSON(&newPeer); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if peer := s.s.peers.GetPeerByKey(newPeer.PublicKey); peer.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "peer already exists"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := s.s.CreatePeer(deviceName, newPeer); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peer := s.s.peers.GetPeerByKey(newPeer.PublicKey)
 | 
				
			||||||
 | 
						if !peer.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, peer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PutPeer godoc
 | 
				
			||||||
 | 
					// @Tags Peers
 | 
				
			||||||
 | 
					// @Summary Updates the given peer based on the given peer model
 | 
				
			||||||
 | 
					// @Accept  json
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param pkey path string true "Public Key"
 | 
				
			||||||
 | 
					// @Param peer body wireguard.Peer true "Peer Model"
 | 
				
			||||||
 | 
					// @Success 200 {object} wireguard.Peer
 | 
				
			||||||
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/peer/{pkey} [put]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) PutPeer(c *gin.Context) {
 | 
				
			||||||
 | 
						updatePeer := wireguard.Peer{}
 | 
				
			||||||
 | 
						if err := c.BindJSON(&updatePeer); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pkey := c.Param("pkey")
 | 
				
			||||||
 | 
						if pkey == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if peer := s.s.peers.GetPeerByKey(pkey); !peer.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Changing public key is not allowed
 | 
				
			||||||
 | 
						if pkey != updatePeer.PublicKey {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						now := time.Now()
 | 
				
			||||||
 | 
						if updatePeer.DeactivatedAt != nil {
 | 
				
			||||||
 | 
							updatePeer.DeactivatedAt = &now
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := s.s.UpdatePeer(updatePeer, now); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peer := s.s.peers.GetPeerByKey(updatePeer.PublicKey)
 | 
				
			||||||
 | 
						if !peer.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, peer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PatchPeer godoc
 | 
				
			||||||
 | 
					// @Tags Peers
 | 
				
			||||||
 | 
					// @Summary Updates the given peer based on the given partial peer model
 | 
				
			||||||
 | 
					// @Accept  json
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param pkey path string true "Public Key"
 | 
				
			||||||
 | 
					// @Param peer body wireguard.Peer true "Peer Model"
 | 
				
			||||||
 | 
					// @Success 200 {object} wireguard.Peer
 | 
				
			||||||
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/peer/{pkey} [patch]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) PatchPeer(c *gin.Context) {
 | 
				
			||||||
 | 
						patch, err := c.GetRawData()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						pkey := c.Param("pkey")
 | 
				
			||||||
 | 
						if pkey == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peer := s.s.peers.GetPeerByKey(pkey)
 | 
				
			||||||
 | 
						if !peer.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peerData, err := json.Marshal(peer)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mergedPeerData, err := jsonpatch.MergePatch(peerData, patch)
 | 
				
			||||||
 | 
						var mergedPeer wireguard.Peer
 | 
				
			||||||
 | 
						err = json.Unmarshal(mergedPeerData, &mergedPeer)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !mergedPeer.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "invalid peer model"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Changing public key is not allowed
 | 
				
			||||||
 | 
						if pkey != mergedPeer.PublicKey {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must match the model public key"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						now := time.Now()
 | 
				
			||||||
 | 
						if mergedPeer.DeactivatedAt != nil {
 | 
				
			||||||
 | 
							mergedPeer.DeactivatedAt = &now
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := s.s.UpdatePeer(mergedPeer, now); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peer = s.s.peers.GetPeerByKey(mergedPeer.PublicKey)
 | 
				
			||||||
 | 
						if !peer.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, peer)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// DeletePeer godoc
 | 
				
			||||||
 | 
					// @Tags Peers
 | 
				
			||||||
 | 
					// @Summary Updates the given peer based on the given partial peer model
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param pkey path string true "Public Key"
 | 
				
			||||||
 | 
					// @Success 202 "No Content"
 | 
				
			||||||
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/peer/{pkey} [delete]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) DeletePeer(c *gin.Context) {
 | 
				
			||||||
 | 
						pkey := c.Param("pkey")
 | 
				
			||||||
 | 
						if pkey == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peer := s.s.peers.GetPeerByKey(pkey)
 | 
				
			||||||
 | 
						if peer.PublicKey == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := s.s.DeletePeer(peer); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Status(http.StatusNoContent)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDevices godoc
 | 
				
			||||||
 | 
					// @Tags Interface
 | 
				
			||||||
 | 
					// @Summary Get all devices
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Success 200 {object} []wireguard.Device
 | 
				
			||||||
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/devices [get]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) GetDevices(c *gin.Context) {
 | 
				
			||||||
 | 
						var devices []wireguard.Device
 | 
				
			||||||
 | 
						for _, deviceName := range s.s.config.WG.DeviceNames {
 | 
				
			||||||
 | 
							device := s.s.peers.GetDevice(deviceName)
 | 
				
			||||||
 | 
							if !device.IsValid() {
 | 
				
			||||||
 | 
								continue
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							devices = append(devices, device)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, devices)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetDevice godoc
 | 
				
			||||||
 | 
					// @Tags Interface
 | 
				
			||||||
 | 
					// @Summary Get the given device
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param device path string true "Device Name"
 | 
				
			||||||
 | 
					// @Success 200 {object} wireguard.Device
 | 
				
			||||||
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/device/{device} [get]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) GetDevice(c *gin.Context) {
 | 
				
			||||||
 | 
						deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
 | 
				
			||||||
 | 
						if deviceName == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// validate device name
 | 
				
			||||||
 | 
						if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						device := s.s.peers.GetDevice(deviceName)
 | 
				
			||||||
 | 
						if !device.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "device not found"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.JSON(http.StatusOK, device)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PutDevice godoc
 | 
				
			||||||
 | 
					// @Tags Interface
 | 
				
			||||||
 | 
					// @Summary Updates the given device based on the given device model (UNIMPLEMENTED)
 | 
				
			||||||
 | 
					// @Accept  json
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param device path string true "Device Name"
 | 
				
			||||||
 | 
					// @Param body body wireguard.Device true "Device Model"
 | 
				
			||||||
 | 
					// @Success 200 {object} wireguard.Device
 | 
				
			||||||
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/device/{device} [put]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) PutDevice(c *gin.Context) {
 | 
				
			||||||
 | 
						updateDevice := wireguard.Device{}
 | 
				
			||||||
 | 
						if err := c.BindJSON(&updateDevice); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
 | 
				
			||||||
 | 
						if deviceName == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// validate device name
 | 
				
			||||||
 | 
						if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						device := s.s.peers.GetDevice(deviceName)
 | 
				
			||||||
 | 
						if !device.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Changing device name is not allowed
 | 
				
			||||||
 | 
						if deviceName != updateDevice.DeviceName {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: implement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.JSON(http.StatusNotImplemented, device)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PatchDevice godoc
 | 
				
			||||||
 | 
					// @Tags Interface
 | 
				
			||||||
 | 
					// @Summary Updates the given device based on the given partial device model (UNIMPLEMENTED)
 | 
				
			||||||
 | 
					// @Accept  json
 | 
				
			||||||
 | 
					// @Produce json
 | 
				
			||||||
 | 
					// @Param device path string true "Device Name"
 | 
				
			||||||
 | 
					// @Param body body wireguard.Device true "Device Model"
 | 
				
			||||||
 | 
					// @Success 200 {object} wireguard.Device
 | 
				
			||||||
 | 
					// @Failure 400 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 500 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /backend/device/{device} [patch]
 | 
				
			||||||
 | 
					// @Security ApiBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) PatchDevice(c *gin.Context) {
 | 
				
			||||||
 | 
						patch, err := c.GetRawData()
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						deviceName := strings.ToLower(strings.TrimSpace(c.Param("device")))
 | 
				
			||||||
 | 
						if deviceName == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// validate device name
 | 
				
			||||||
 | 
						if !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "unknown device"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						device := s.s.peers.GetDevice(deviceName)
 | 
				
			||||||
 | 
						if !device.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer not found"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						deviceData, err := json.Marshal(device)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mergedDeviceData, err := jsonpatch.MergePatch(deviceData, patch)
 | 
				
			||||||
 | 
						var mergedDevice wireguard.Device
 | 
				
			||||||
 | 
						err = json.Unmarshal(mergedDeviceData, &mergedDevice)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !mergedDevice.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "invalid device model"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Changing device name is not allowed
 | 
				
			||||||
 | 
						if deviceName != mergedDevice.DeviceName {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "device parameter must match the model device name"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: implement
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.JSON(http.StatusNotImplemented, device)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// GetPeerDeploymentConfig godoc
 | 
				
			||||||
 | 
					// @Tags Provisioning
 | 
				
			||||||
 | 
					// @Summary Retrieves the peer config for the given public key
 | 
				
			||||||
 | 
					// @Produce plain
 | 
				
			||||||
 | 
					// @Param pkey path string true "Public Key (Base 64)"
 | 
				
			||||||
 | 
					// @Success 200 {object} string "The WireGuard configuration file"
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /provisioning/peer/{pkey} [get]
 | 
				
			||||||
 | 
					// @Security GeneralBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) GetPeerDeploymentConfig(c *gin.Context) {
 | 
				
			||||||
 | 
						pkey := c.Param("pkey")
 | 
				
			||||||
 | 
						if pkey == "" {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: "pkey parameter must be specified"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						peer := s.s.peers.GetPeerByKey(pkey)
 | 
				
			||||||
 | 
						if !peer.IsValid() {
 | 
				
			||||||
 | 
							c.JSON(http.StatusNotFound, ApiError{Message: "peer does not exist"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get authenticated user to check permissions
 | 
				
			||||||
 | 
						username, _, _ := c.Request.BasicAuth()
 | 
				
			||||||
 | 
						user := s.s.users.GetUser(username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user.IsAdmin && user.Email == peer.Email {
 | 
				
			||||||
 | 
							c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						device := s.s.peers.GetDevice(peer.DeviceName)
 | 
				
			||||||
 | 
						config, err := peer.GetConfigFile(device)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Data(http.StatusOK, "text/plain", config)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type ProvisioningRequest struct {
 | 
				
			||||||
 | 
						// DeviceName is optional, if not specified, the configured default device will be used.
 | 
				
			||||||
 | 
						DeviceName string `json:",omitempty"`
 | 
				
			||||||
 | 
						Identifier string `binding:"required"`
 | 
				
			||||||
 | 
						Email      string `binding:"required"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Client specific and optional settings
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						AllowedIPsStr       string `binding:"cidrlist" json:",omitempty"`
 | 
				
			||||||
 | 
						PersistentKeepalive int    `binding:"gte=0" json:",omitempty"`
 | 
				
			||||||
 | 
						DNSStr              string `binding:"iplist" json:",omitempty"`
 | 
				
			||||||
 | 
						Mtu                 int    `binding:"gte=0,lte=1500" json:",omitempty"`
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// PostPeerDeploymentConfig godoc
 | 
				
			||||||
 | 
					// @Tags Provisioning
 | 
				
			||||||
 | 
					// @Summary Creates the requested peer config and returns the config file
 | 
				
			||||||
 | 
					// @Accept  json
 | 
				
			||||||
 | 
					// @Produce plain
 | 
				
			||||||
 | 
					// @Param body body ProvisioningRequest true "Provisioning Request Model"
 | 
				
			||||||
 | 
					// @Success 200 {object} string "The WireGuard configuration file"
 | 
				
			||||||
 | 
					// @Failure 401 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 403 {object} ApiError
 | 
				
			||||||
 | 
					// @Failure 404 {object} ApiError
 | 
				
			||||||
 | 
					// @Router /provisioning/peer [post]
 | 
				
			||||||
 | 
					// @Security GeneralBasicAuth
 | 
				
			||||||
 | 
					func (s *ApiServer) PostPeerDeploymentConfig(c *gin.Context) {
 | 
				
			||||||
 | 
						req := ProvisioningRequest{}
 | 
				
			||||||
 | 
						if err := c.BindJSON(&req); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Get authenticated user to check permissions
 | 
				
			||||||
 | 
						username, _, _ := c.Request.BasicAuth()
 | 
				
			||||||
 | 
						user := s.s.users.GetUser(username)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user.IsAdmin && !s.s.config.Core.SelfProvisioningAllowed {
 | 
				
			||||||
 | 
							c.JSON(http.StatusForbidden, ApiError{Message: "peer provisioning service disabled"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !user.IsAdmin && user.Email == req.Email {
 | 
				
			||||||
 | 
							c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						deviceName := req.DeviceName
 | 
				
			||||||
 | 
						if deviceName == "" || !common.ListContains(s.s.config.WG.DeviceNames, deviceName) {
 | 
				
			||||||
 | 
							deviceName = s.s.config.WG.GetDefaultDeviceName()
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						device := s.s.peers.GetDevice(deviceName)
 | 
				
			||||||
 | 
						if device.Type != wireguard.DeviceTypeServer {
 | 
				
			||||||
 | 
							c.JSON(http.StatusForbidden, ApiError{Message: "invalid device, provisioning disabled"})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// check if private/public keys are set, if so check database for existing entries
 | 
				
			||||||
 | 
						peer, err := s.s.PrepareNewPeer(deviceName)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						peer.Email = req.Email
 | 
				
			||||||
 | 
						peer.Identifier = req.Identifier
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if req.AllowedIPsStr != "" {
 | 
				
			||||||
 | 
							peer.AllowedIPsStr = req.AllowedIPsStr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if req.PersistentKeepalive != 0 {
 | 
				
			||||||
 | 
							peer.PersistentKeepalive = req.PersistentKeepalive
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if req.DNSStr != "" {
 | 
				
			||||||
 | 
							peer.DNSStr = req.DNSStr
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if req.Mtu != 0 {
 | 
				
			||||||
 | 
							peer.Mtu = req.Mtu
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if err := s.s.CreatePeer(deviceName, peer); err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						config, err := peer.GetConfigFile(device)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c.Data(http.StatusOK, "text/plain", config)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -55,17 +55,18 @@ func loadConfigEnv(cfg interface{}) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
type Config struct {
 | 
					type Config struct {
 | 
				
			||||||
	Core struct {
 | 
						Core struct {
 | 
				
			||||||
		ListeningAddress  string `yaml:"listeningAddress" envconfig:"LISTENING_ADDRESS"`
 | 
							ListeningAddress        string `yaml:"listeningAddress" envconfig:"LISTENING_ADDRESS"`
 | 
				
			||||||
		ExternalUrl       string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"`
 | 
							ExternalUrl             string `yaml:"externalUrl" envconfig:"EXTERNAL_URL"`
 | 
				
			||||||
		Title             string `yaml:"title" envconfig:"WEBSITE_TITLE"`
 | 
							Title                   string `yaml:"title" envconfig:"WEBSITE_TITLE"`
 | 
				
			||||||
		CompanyName       string `yaml:"company" envconfig:"COMPANY_NAME"`
 | 
							CompanyName             string `yaml:"company" envconfig:"COMPANY_NAME"`
 | 
				
			||||||
		MailFrom          string `yaml:"mailFrom" envconfig:"MAIL_FROM"`
 | 
							MailFrom                string `yaml:"mailFrom" envconfig:"MAIL_FROM"`
 | 
				
			||||||
		AdminUser         string `yaml:"adminUser" envconfig:"ADMIN_USER"` // must be an email address
 | 
							AdminUser               string `yaml:"adminUser" envconfig:"ADMIN_USER"` // must be an email address
 | 
				
			||||||
		AdminPassword     string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
 | 
							AdminPassword           string `yaml:"adminPass" envconfig:"ADMIN_PASS"`
 | 
				
			||||||
		EditableKeys      bool   `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
 | 
							EditableKeys            bool   `yaml:"editableKeys" envconfig:"EDITABLE_KEYS"`
 | 
				
			||||||
		CreateDefaultPeer bool   `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"`
 | 
							CreateDefaultPeer       bool   `yaml:"createDefaultPeer" envconfig:"CREATE_DEFAULT_PEER"`
 | 
				
			||||||
		LdapEnabled       bool   `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
 | 
							SelfProvisioningAllowed bool   `yaml:"selfProvisioning" envconfig:"SELF_PROVISIONING"`
 | 
				
			||||||
		SessionSecret     string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
 | 
							LdapEnabled             bool   `yaml:"ldapEnabled" envconfig:"LDAP_ENABLED"`
 | 
				
			||||||
 | 
							SessionSecret           string `yaml:"sessionSecret" envconfig:"SESSION_SECRET"`
 | 
				
			||||||
	} `yaml:"core"`
 | 
						} `yaml:"core"`
 | 
				
			||||||
	Database common.DatabaseConfig `yaml:"database"`
 | 
						Database common.DatabaseConfig `yaml:"database"`
 | 
				
			||||||
	Email    common.MailConfig     `yaml:"email"`
 | 
						Email    common.MailConfig     `yaml:"email"`
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -8,7 +8,6 @@ import (
 | 
				
			|||||||
	"github.com/gin-gonic/gin"
 | 
						"github.com/gin-gonic/gin"
 | 
				
			||||||
	"github.com/h44z/wg-portal/internal/users"
 | 
						"github.com/h44z/wg-portal/internal/users"
 | 
				
			||||||
	csrf "github.com/utrack/gin-csrf"
 | 
						csrf "github.com/utrack/gin-csrf"
 | 
				
			||||||
	"golang.org/x/crypto/bcrypt"
 | 
					 | 
				
			||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -105,19 +104,6 @@ func (s *Server) PostAdminUsersEdit(c *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if formUser.Password != "" {
 | 
					 | 
				
			||||||
		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(formUser.Password), bcrypt.DefaultCost)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			_ = s.updateFormInSession(c, formUser)
 | 
					 | 
				
			||||||
			SetFlashMessage(c, "failed to hash admin password", "danger")
 | 
					 | 
				
			||||||
			c.Redirect(http.StatusSeeOther, "/admin/users/edit?pkey="+urlEncodedKey+"&formerr=bind")
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		formUser.Password = string(hashedPassword)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		formUser.Password = currentUser.Password
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	disabled := c.PostForm("isdisabled") != ""
 | 
						disabled := c.PostForm("isdisabled") != ""
 | 
				
			||||||
	if disabled {
 | 
						if disabled {
 | 
				
			||||||
		formUser.DeletedAt = gorm.DeletedAt{
 | 
							formUser.DeletedAt = gorm.DeletedAt{
 | 
				
			||||||
@@ -175,15 +161,7 @@ func (s *Server) PostAdminUsersCreate(c *gin.Context) {
 | 
				
			|||||||
		return
 | 
							return
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if formUser.Password != "" {
 | 
						if formUser.Password == "" {
 | 
				
			||||||
		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(formUser.Password), bcrypt.DefaultCost)
 | 
					 | 
				
			||||||
		if err != nil {
 | 
					 | 
				
			||||||
			SetFlashMessage(c, "failed to hash admin password", "danger")
 | 
					 | 
				
			||||||
			c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=bind")
 | 
					 | 
				
			||||||
			return
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		formUser.Password = string(hashedPassword)
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		_ = s.updateFormInSession(c, formUser)
 | 
							_ = s.updateFormInSession(c, formUser)
 | 
				
			||||||
		SetFlashMessage(c, "invalid password", "danger")
 | 
							SetFlashMessage(c, "invalid password", "danger")
 | 
				
			||||||
		c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create")
 | 
							c.Redirect(http.StatusSeeOther, "/admin/users/create?formerr=create")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,9 +10,18 @@ import (
 | 
				
			|||||||
	_ "github.com/h44z/wg-portal/internal/server/docs" // docs is generated by Swag CLI, you have to import it.
 | 
						_ "github.com/h44z/wg-portal/internal/server/docs" // docs is generated by Swag CLI, you have to import it.
 | 
				
			||||||
	ginSwagger "github.com/swaggo/gin-swagger"
 | 
						ginSwagger "github.com/swaggo/gin-swagger"
 | 
				
			||||||
	"github.com/swaggo/gin-swagger/swaggerFiles"
 | 
						"github.com/swaggo/gin-swagger/swaggerFiles"
 | 
				
			||||||
 | 
						csrf "github.com/utrack/gin-csrf"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func SetupRoutes(s *Server) {
 | 
					func SetupRoutes(s *Server) {
 | 
				
			||||||
 | 
						csrfMiddleware := csrf.Middleware(csrf.Options{
 | 
				
			||||||
 | 
							Secret: s.config.Core.SessionSecret,
 | 
				
			||||||
 | 
							ErrorFunc: func(c *gin.Context) {
 | 
				
			||||||
 | 
								c.String(400, "CSRF token mismatch")
 | 
				
			||||||
 | 
								c.Abort()
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Startpage
 | 
						// Startpage
 | 
				
			||||||
	s.server.GET("/", s.GetIndex)
 | 
						s.server.GET("/", s.GetIndex)
 | 
				
			||||||
	s.server.GET("/favicon.ico", func(c *gin.Context) {
 | 
						s.server.GET("/favicon.ico", func(c *gin.Context) {
 | 
				
			||||||
@@ -26,12 +35,14 @@ func SetupRoutes(s *Server) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// Auth routes
 | 
						// Auth routes
 | 
				
			||||||
	auth := s.server.Group("/auth")
 | 
						auth := s.server.Group("/auth")
 | 
				
			||||||
 | 
						auth.Use(csrfMiddleware)
 | 
				
			||||||
	auth.GET("/login", s.GetLogin)
 | 
						auth.GET("/login", s.GetLogin)
 | 
				
			||||||
	auth.POST("/login", s.PostLogin)
 | 
						auth.POST("/login", s.PostLogin)
 | 
				
			||||||
	auth.GET("/logout", s.GetLogout)
 | 
						auth.GET("/logout", s.GetLogout)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Admin routes
 | 
						// Admin routes
 | 
				
			||||||
	admin := s.server.Group("/admin")
 | 
						admin := s.server.Group("/admin")
 | 
				
			||||||
 | 
						admin.Use(csrfMiddleware)
 | 
				
			||||||
	admin.Use(s.RequireAuthentication("admin"))
 | 
						admin.Use(s.RequireAuthentication("admin"))
 | 
				
			||||||
	admin.GET("/", s.GetAdminIndex)
 | 
						admin.GET("/", s.GetAdminIndex)
 | 
				
			||||||
	admin.GET("/device/edit", s.GetAdminEditInterface)
 | 
						admin.GET("/device/edit", s.GetAdminEditInterface)
 | 
				
			||||||
@@ -57,6 +68,7 @@ func SetupRoutes(s *Server) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	// User routes
 | 
						// User routes
 | 
				
			||||||
	user := s.server.Group("/user")
 | 
						user := s.server.Group("/user")
 | 
				
			||||||
 | 
						user.Use(csrfMiddleware)
 | 
				
			||||||
	user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
 | 
						user.Use(s.RequireAuthentication("")) // empty scope = all logged in users
 | 
				
			||||||
	user.GET("/qrcode", s.GetPeerQRCode)
 | 
						user.GET("/qrcode", s.GetPeerQRCode)
 | 
				
			||||||
	user.GET("/profile", s.GetUserIndex)
 | 
						user.GET("/profile", s.GetUserIndex)
 | 
				
			||||||
@@ -68,15 +80,35 @@ func SetupRoutes(s *Server) {
 | 
				
			|||||||
func SetupApiRoutes(s *Server) {
 | 
					func SetupApiRoutes(s *Server) {
 | 
				
			||||||
	api := ApiServer{s: s}
 | 
						api := ApiServer{s: s}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Auth routes
 | 
						// Admin authenticated routes
 | 
				
			||||||
	apiV1 := s.server.Group("/api/v1")
 | 
						apiV1Backend := s.server.Group("/api/v1/backend")
 | 
				
			||||||
	apiV1.Use(s.RequireApiAuthentication("admin"))
 | 
						apiV1Backend.Use(s.RequireApiAuthentication("admin"))
 | 
				
			||||||
	apiV1.GET("/users", api.GetUsers)
 | 
					
 | 
				
			||||||
	apiV1.POST("/users", api.PostUser)
 | 
						apiV1Backend.GET("/users", api.GetUsers)
 | 
				
			||||||
	apiV1.GET("/user/:email", api.GetUser)
 | 
						apiV1Backend.POST("/users", api.PostUser)
 | 
				
			||||||
	apiV1.PUT("/user/:email", api.PutUser)
 | 
						apiV1Backend.GET("/user/:email", api.GetUser)
 | 
				
			||||||
	apiV1.PATCH("/user/:email", api.PatchUser)
 | 
						apiV1Backend.PUT("/user/:email", api.PutUser)
 | 
				
			||||||
	apiV1.DELETE("/user/:email", api.DeleteUser)
 | 
						apiV1Backend.PATCH("/user/:email", api.PatchUser)
 | 
				
			||||||
 | 
						apiV1Backend.DELETE("/user/:email", api.DeleteUser)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiV1Backend.GET("/peers/:device", api.GetPeers)
 | 
				
			||||||
 | 
						apiV1Backend.POST("/peers/:device", api.PostPeer)
 | 
				
			||||||
 | 
						apiV1Backend.GET("/peer/:pkey", api.GetPeer)
 | 
				
			||||||
 | 
						apiV1Backend.PUT("/peer/:pkey", api.PutPeer)
 | 
				
			||||||
 | 
						apiV1Backend.PATCH("/peer/:pkey", api.PatchPeer)
 | 
				
			||||||
 | 
						apiV1Backend.DELETE("/peer/:pkey", api.DeletePeer)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiV1Backend.GET("/devices", api.GetDevices)
 | 
				
			||||||
 | 
						apiV1Backend.GET("/device/:device", api.GetDevice)
 | 
				
			||||||
 | 
						apiV1Backend.PUT("/device/:device", api.PutDevice)
 | 
				
			||||||
 | 
						apiV1Backend.PATCH("/device/:device", api.PatchDevice)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Simple authenticated routes
 | 
				
			||||||
 | 
						apiV1Deployment := s.server.Group("/api/v1/provisioning")
 | 
				
			||||||
 | 
						apiV1Deployment.Use(s.RequireApiAuthentication(""))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						apiV1Deployment.GET("/peer/:pkey", api.GetPeerDeploymentConfig)
 | 
				
			||||||
 | 
						apiV1Deployment.POST("/peer", api.PostPeerDeploymentConfig)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Swagger doc/ui
 | 
						// Swagger doc/ui
 | 
				
			||||||
	s.server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
 | 
						s.server.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,7 +26,6 @@ import (
 | 
				
			|||||||
	"github.com/pkg/errors"
 | 
						"github.com/pkg/errors"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
	ginlogrus "github.com/toorop/gin-logrus"
 | 
						ginlogrus "github.com/toorop/gin-logrus"
 | 
				
			||||||
	csrf "github.com/utrack/gin-csrf"
 | 
					 | 
				
			||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -118,13 +117,6 @@ func (s *Server) Setup(ctx context.Context) error {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	s.server.Use(gin.Recovery())
 | 
						s.server.Use(gin.Recovery())
 | 
				
			||||||
	s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte(s.config.Core.SessionSecret))))
 | 
						s.server.Use(sessions.Sessions("authsession", memstore.NewStore([]byte(s.config.Core.SessionSecret))))
 | 
				
			||||||
	s.server.Use(csrf.Middleware(csrf.Options{
 | 
					 | 
				
			||||||
		Secret: s.config.Core.SessionSecret,
 | 
					 | 
				
			||||||
		ErrorFunc: func(c *gin.Context) {
 | 
					 | 
				
			||||||
			c.String(400, "CSRF token mismatch")
 | 
					 | 
				
			||||||
			c.Abort()
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
	}))
 | 
					 | 
				
			||||||
	s.server.SetFuncMap(template.FuncMap{
 | 
						s.server.SetFuncMap(template.FuncMap{
 | 
				
			||||||
		"formatBytes": common.ByteCountSI,
 | 
							"formatBytes": common.ByteCountSI,
 | 
				
			||||||
		"urlEncode":   url.QueryEscape,
 | 
							"urlEncode":   url.QueryEscape,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@ import (
 | 
				
			|||||||
	"github.com/h44z/wg-portal/internal/wireguard"
 | 
						"github.com/h44z/wg-portal/internal/wireguard"
 | 
				
			||||||
	"github.com/pkg/errors"
 | 
						"github.com/pkg/errors"
 | 
				
			||||||
	"github.com/sirupsen/logrus"
 | 
						"github.com/sirupsen/logrus"
 | 
				
			||||||
 | 
						"golang.org/x/crypto/bcrypt"
 | 
				
			||||||
	"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 | 
						"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
 | 
				
			||||||
	"gorm.io/gorm"
 | 
						"gorm.io/gorm"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@@ -52,6 +53,7 @@ func (s *Server) PrepareNewPeer(device string) (wireguard.Peer, error) {
 | 
				
			|||||||
		peer.PersistentKeepalive = dev.DefaultPersistentKeepalive
 | 
							peer.PersistentKeepalive = dev.DefaultPersistentKeepalive
 | 
				
			||||||
		peer.AllowedIPsStr = dev.DefaultAllowedIPsStr
 | 
							peer.AllowedIPsStr = dev.DefaultAllowedIPsStr
 | 
				
			||||||
		peer.Mtu = dev.Mtu
 | 
							peer.Mtu = dev.Mtu
 | 
				
			||||||
 | 
							peer.DeviceName = device
 | 
				
			||||||
	case wireguard.DeviceTypeClient:
 | 
						case wireguard.DeviceTypeClient:
 | 
				
			||||||
		peer.UID = "newendpoint"
 | 
							peer.UID = "newendpoint"
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -225,6 +227,15 @@ func (s *Server) CreateUser(user users.User, device string) error {
 | 
				
			|||||||
		return s.UpdateUser(user)
 | 
							return s.UpdateUser(user)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Hash user password (if set)
 | 
				
			||||||
 | 
						if user.Password != "" {
 | 
				
			||||||
 | 
							hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return errors.Wrap(err, "unable to hash password")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							user.Password = users.PrivateString(hashedPassword)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Create user in database
 | 
						// Create user in database
 | 
				
			||||||
	if err := s.users.CreateUser(&user); err != nil {
 | 
						if err := s.users.CreateUser(&user); err != nil {
 | 
				
			||||||
		return errors.WithMessage(err, "failed to create user in manager")
 | 
							return errors.WithMessage(err, "failed to create user in manager")
 | 
				
			||||||
@@ -243,6 +254,17 @@ func (s *Server) UpdateUser(user users.User) error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	currentUser := s.users.GetUserUnscoped(user.Email)
 | 
						currentUser := s.users.GetUserUnscoped(user.Email)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Hash user password (if set)
 | 
				
			||||||
 | 
						if user.Password != "" {
 | 
				
			||||||
 | 
							hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
 | 
				
			||||||
 | 
							if err != nil {
 | 
				
			||||||
 | 
								return errors.Wrap(err, "unable to hash password")
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							user.Password = users.PrivateString(hashedPassword)
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							user.Password = currentUser.Password // keep current password
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Update in database
 | 
						// Update in database
 | 
				
			||||||
	if err := s.users.UpdateUser(&user); err != nil {
 | 
						if err := s.users.UpdateUser(&user); err != nil {
 | 
				
			||||||
		return errors.WithMessage(err, "failed to update user in manager")
 | 
							return errors.WithMessage(err, "failed to update user in manager")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -142,6 +142,7 @@ func (m Manager) GetOrCreateUserUnscoped(email string) (*User, error) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
func (m Manager) CreateUser(user *User) error {
 | 
					func (m Manager) CreateUser(user *User) error {
 | 
				
			||||||
	user.Email = strings.ToLower(user.Email)
 | 
						user.Email = strings.ToLower(user.Email)
 | 
				
			||||||
 | 
						user.Source = UserSourceDatabase
 | 
				
			||||||
	res := m.db.Create(user)
 | 
						res := m.db.Create(user)
 | 
				
			||||||
	if res.Error != nil {
 | 
						if res.Error != nil {
 | 
				
			||||||
		return errors.Wrapf(res.Error, "failed to create user %s", user.Email)
 | 
							return errors.Wrapf(res.Error, "failed to create user %s", user.Email)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,16 @@ const (
 | 
				
			|||||||
	UserSourceOIDC     UserSource = "oidc" // open id connect, TODO: implement
 | 
						UserSourceOIDC     UserSource = "oidc" // open id connect, TODO: implement
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type PrivateString string
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (PrivateString) MarshalJSON() ([]byte, error) {
 | 
				
			||||||
 | 
						return []byte(`""`), nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					func (PrivateString) String() string {
 | 
				
			||||||
 | 
						return ""
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// User is the user model that gets linked to peer entries, by default an empty usermodel with only the email address is created
 | 
					// User is the user model that gets linked to peer entries, by default an empty usermodel with only the email address is created
 | 
				
			||||||
type User struct {
 | 
					type User struct {
 | 
				
			||||||
	// required fields
 | 
						// required fields
 | 
				
			||||||
@@ -27,10 +37,10 @@ type User struct {
 | 
				
			|||||||
	Phone     string `form:"phone" binding:"omitempty"`
 | 
						Phone     string `form:"phone" binding:"omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// optional, integrated password authentication
 | 
						// optional, integrated password authentication
 | 
				
			||||||
	Password string `form:"password" binding:"omitempty"`
 | 
						Password PrivateString `form:"password" binding:"omitempty"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// database internal fields
 | 
						// database internal fields
 | 
				
			||||||
	CreatedAt time.Time
 | 
						CreatedAt time.Time
 | 
				
			||||||
	UpdatedAt time.Time
 | 
						UpdatedAt time.Time
 | 
				
			||||||
	DeletedAt gorm.DeletedAt `gorm:"index"`
 | 
						DeletedAt gorm.DeletedAt `gorm:"index" json:",omitempty"`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -63,21 +63,21 @@ func init() {
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Peer struct {
 | 
					type Peer struct {
 | 
				
			||||||
	Peer   *wgtypes.Peer `gorm:"-"`                                 // WireGuard peer
 | 
						Peer   *wgtypes.Peer `gorm:"-" json:"-"`                                 // WireGuard peer
 | 
				
			||||||
	Device *Device       `gorm:"foreignKey:DeviceName" binding:"-"` // linked WireGuard device
 | 
						Device *Device       `gorm:"foreignKey:DeviceName" binding:"-" json:"-"` // linked WireGuard device
 | 
				
			||||||
	Config string        `gorm:"-"`
 | 
						Config string        `gorm:"-" json:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	UID                  string     `form:"uid" binding:"required,alphanum"` // uid for html identification
 | 
						UID                  string     `form:"uid" binding:"required,alphanum" json:"-"` // uid for html identification
 | 
				
			||||||
	DeviceName           string     `gorm:"index" form:"device" binding:"required"`
 | 
						DeviceName           string     `gorm:"index" form:"device" binding:"required"`
 | 
				
			||||||
	DeviceType           DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server"`
 | 
						DeviceType           DeviceType `gorm:"-" form:"devicetype" binding:"required,oneof=client server" json:"-"`
 | 
				
			||||||
	Identifier           string     `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique
 | 
						Identifier           string     `form:"identifier" binding:"required,max=64"` // Identifier AND Email make a WireGuard peer unique
 | 
				
			||||||
	Email                string     `gorm:"index" form:"mail" binding:"required,email"`
 | 
						Email                string     `gorm:"index" form:"mail" binding:"required,email"`
 | 
				
			||||||
	IgnoreGlobalSettings bool       `form:"ignoreglobalsettings"`
 | 
						IgnoreGlobalSettings bool       `form:"ignoreglobalsettings"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	IsOnline          bool   `gorm:"-"`
 | 
						IsOnline          bool   `gorm:"-" json:"-"`
 | 
				
			||||||
	IsNew             bool   `gorm:"-"`
 | 
						IsNew             bool   `gorm:"-" json:"-"`
 | 
				
			||||||
	LastHandshake     string `gorm:"-"`
 | 
						LastHandshake     string `gorm:"-" json:"-"`
 | 
				
			||||||
	LastHandshakeTime string `gorm:"-"`
 | 
						LastHandshakeTime string `gorm:"-" json:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Core WireGuard Settings
 | 
						// Core WireGuard Settings
 | 
				
			||||||
	PublicKey           string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` // the public key of the peer itself
 | 
						PublicKey           string `gorm:"primaryKey" form:"pubkey" binding:"required,base64"` // the public key of the peer itself
 | 
				
			||||||
@@ -93,7 +93,7 @@ type Peer struct {
 | 
				
			|||||||
	// Global Device Settings (can be ignored, only make sense if device is in server mode)
 | 
						// Global Device Settings (can be ignored, only make sense if device is in server mode)
 | 
				
			||||||
	Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
 | 
						Mtu int `form:"mtu" binding:"gte=0,lte=1500"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	DeactivatedAt *time.Time
 | 
						DeactivatedAt *time.Time `json:",omitempty"`
 | 
				
			||||||
	CreatedBy     string
 | 
						CreatedBy     string
 | 
				
			||||||
	UpdatedBy     string
 | 
						UpdatedBy     string
 | 
				
			||||||
	CreatedAt     time.Time
 | 
						CreatedAt     time.Time
 | 
				
			||||||
@@ -226,7 +226,7 @@ const (
 | 
				
			|||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Device struct {
 | 
					type Device struct {
 | 
				
			||||||
	Interface *wgtypes.Device `gorm:"-"`
 | 
						Interface *wgtypes.Device `gorm:"-" json:"-"`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	Type        DeviceType `form:"devicetype" binding:"required,oneof=client server"`
 | 
						Type        DeviceType `form:"devicetype" binding:"required,oneof=client server"`
 | 
				
			||||||
	DeviceName  string     `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
 | 
						DeviceName  string     `form:"device" gorm:"primaryKey" binding:"required,alphanum"`
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user