mirror of https://github.com/h44z/wg-portal.git
				
				
				
			
		
			
				
	
	
		
			927 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			927 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
| package server
 | |
| 
 | |
| // go get -u github.com/swaggo/swag/cmd/swag
 | |
| // run: swag init --parseDependency --parseInternal --generalInfo api.go
 | |
| // in the internal/server folder
 | |
| import (
 | |
| 	"encoding/json"
 | |
| 	"net/http"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	jsonpatch "github.com/evanphx/json-patch"
 | |
| 	"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/wireguard"
 | |
| )
 | |
| 
 | |
| // @title WireGuard Portal API
 | |
| // @version 1.0
 | |
| // @description WireGuard Portal API for managing users and peers.
 | |
| 
 | |
| // @license.name MIT
 | |
| // @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
 | |
| // @in header
 | |
| // @name Authorization
 | |
| // @scope.admin Admin access required
 | |
| 
 | |
| // @securityDefinitions.basic GeneralBasicAuth
 | |
| // @in header
 | |
| // @name Authorization
 | |
| // @scope.user User access required
 | |
| 
 | |
| // @BasePath /api/v1
 | |
| 
 | |
| // ApiServer is a simple wrapper struct so that we can have fresh member function names.
 | |
| type ApiServer struct {
 | |
| 	s *Server
 | |
| }
 | |
| 
 | |
| type ApiError struct {
 | |
| 	Message string
 | |
| }
 | |
| 
 | |
| // GetUsers godoc
 | |
| // @Tags Users
 | |
| // @Summary Retrieves all users
 | |
| // @Produce json
 | |
| // @Success 200 {object} []users.User
 | |
| // @Failure 401 {object} ApiError
 | |
| // @Failure 403 {object} ApiError
 | |
| // @Failure 404 {object} ApiError
 | |
| // @Router /backend/users [get]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) GetUsers(c *gin.Context) {
 | |
| 	allUsers := s.s.users.GetUsersUnscoped()
 | |
| 
 | |
| 	c.JSON(http.StatusOK, allUsers)
 | |
| }
 | |
| 
 | |
| // GetUser godoc
 | |
| // @Tags Users
 | |
| // @Summary Retrieves user based on given Email
 | |
| // @Produce json
 | |
| // @Param email query string true "User Email"
 | |
| // @Success 200 {object} users.User
 | |
| // @Failure 400 {object} ApiError
 | |
| // @Failure 401 {object} ApiError
 | |
| // @Failure 403 {object} ApiError
 | |
| // @Failure 404 {object} ApiError
 | |
| // @Router /backend/user [get]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) GetUser(c *gin.Context) {
 | |
| 	email := strings.ToLower(strings.TrimSpace(c.Query("email")))
 | |
| 	if email == "" {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	user := s.s.users.GetUserUnscoped(email)
 | |
| 	if user == nil {
 | |
| 		c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | |
| 		return
 | |
| 	}
 | |
| 	c.JSON(http.StatusOK, user)
 | |
| }
 | |
| 
 | |
| // PostUser godoc
 | |
| // @Tags Users
 | |
| // @Summary Creates a new user based on the given user model
 | |
| // @Accept  json
 | |
| // @Produce json
 | |
| // @Param user body users.User true "User Model"
 | |
| // @Success 200 {object} users.User
 | |
| // @Failure 400 {object} ApiError
 | |
| // @Failure 401 {object} ApiError
 | |
| // @Failure 403 {object} ApiError
 | |
| // @Failure 404 {object} ApiError
 | |
| // @Failure 500 {object} ApiError
 | |
| // @Router /backend/users [post]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) PostUser(c *gin.Context) {
 | |
| 	newUser := users.User{}
 | |
| 	if err := c.BindJSON(&newUser); err != nil {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if user := s.s.users.GetUserUnscoped(newUser.Email); user != nil {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: "user already exists"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := s.s.CreateUser(newUser, s.s.wg.Cfg.GetDefaultDeviceName()); err != nil {
 | |
| 		c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	user := s.s.users.GetUserUnscoped(newUser.Email)
 | |
| 	if user == nil {
 | |
| 		c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | |
| 		return
 | |
| 	}
 | |
| 	c.JSON(http.StatusOK, user)
 | |
| }
 | |
| 
 | |
| // PutUser godoc
 | |
| // @Tags Users
 | |
| // @Summary Updates a user based on the given user model
 | |
| // @Accept  json
 | |
| // @Produce json
 | |
| // @Param email query string true "User Email"
 | |
| // @Param user body users.User true "User Model"
 | |
| // @Success 200 {object} users.User
 | |
| // @Failure 400 {object} ApiError
 | |
| // @Failure 401 {object} ApiError
 | |
| // @Failure 403 {object} ApiError
 | |
| // @Failure 404 {object} ApiError
 | |
| // @Failure 500 {object} ApiError
 | |
| // @Router /backend/user [put]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) PutUser(c *gin.Context) {
 | |
| 	email := strings.ToLower(strings.TrimSpace(c.Query("email")))
 | |
| 	if email == "" {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	updateUser := users.User{}
 | |
| 	if err := c.BindJSON(&updateUser); err != nil {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Changing email address is not allowed
 | |
| 	if email != updateUser.Email {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if user := s.s.users.GetUserUnscoped(email); user == nil {
 | |
| 		c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := s.s.UpdateUser(updateUser); err != nil {
 | |
| 		c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	user := s.s.users.GetUserUnscoped(email)
 | |
| 	if user == nil {
 | |
| 		c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | |
| 		return
 | |
| 	}
 | |
| 	c.JSON(http.StatusOK, user)
 | |
| }
 | |
| 
 | |
| // PatchUser godoc
 | |
| // @Tags Users
 | |
| // @Summary Updates a user based on the given partial user model
 | |
| // @Accept  json
 | |
| // @Produce json
 | |
| // @Param email query string true "User Email"
 | |
| // @Param user body users.User true "User Model"
 | |
| // @Success 200 {object} users.User
 | |
| // @Failure 400 {object} ApiError
 | |
| // @Failure 401 {object} ApiError
 | |
| // @Failure 403 {object} ApiError
 | |
| // @Failure 404 {object} ApiError
 | |
| // @Failure 500 {object} ApiError
 | |
| // @Router /backend/user [patch]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) PatchUser(c *gin.Context) {
 | |
| 	email := strings.ToLower(strings.TrimSpace(c.Query("email")))
 | |
| 	if email == "" {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	patch, err := c.GetRawData()
 | |
| 	if err != nil {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: err.Error()})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	user := s.s.users.GetUserUnscoped(email)
 | |
| 	if user == nil {
 | |
| 		c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"})
 | |
| 		return
 | |
| 	}
 | |
| 	userData, err := json.Marshal(user)
 | |
| 	if err != nil {
 | |
| 		c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	mergedUserData, err := jsonpatch.MergePatch(userData, patch)
 | |
| 	var mergedUser users.User
 | |
| 	err = json.Unmarshal(mergedUserData, &mergedUser)
 | |
| 	if err != nil {
 | |
| 		c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// CHanging email address is not allowed
 | |
| 	if email != mergedUser.Email {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must match the model email address"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := s.s.UpdateUser(mergedUser); err != nil {
 | |
| 		c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	user = s.s.users.GetUserUnscoped(email)
 | |
| 	if user == nil {
 | |
| 		c.JSON(http.StatusNotFound, ApiError{Message: "user not found"})
 | |
| 		return
 | |
| 	}
 | |
| 	c.JSON(http.StatusOK, user)
 | |
| }
 | |
| 
 | |
| // DeleteUser godoc
 | |
| // @Tags Users
 | |
| // @Summary Deletes the specified user
 | |
| // @Produce json
 | |
| // @Param email query string true "User Email"
 | |
| // @Success 204 "No content"
 | |
| // @Failure 400 {object} ApiError
 | |
| // @Failure 401 {object} ApiError
 | |
| // @Failure 403 {object} ApiError
 | |
| // @Failure 404 {object} ApiError
 | |
| // @Failure 500 {object} ApiError
 | |
| // @Router /backend/user [delete]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) DeleteUser(c *gin.Context) {
 | |
| 	email := strings.ToLower(strings.TrimSpace(c.Query("email")))
 | |
| 	if email == "" {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	var user *users.User
 | |
| 	if user = s.s.users.GetUserUnscoped(email); user == nil {
 | |
| 		c.JSON(http.StatusNotFound, ApiError{Message: "user does not exist"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if err := s.s.DeleteUser(*user); err != nil {
 | |
| 		c.JSON(http.StatusInternalServerError, ApiError{Message: err.Error()})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	c.Status(http.StatusNoContent)
 | |
| }
 | |
| 
 | |
| // GetPeers godoc
 | |
| // @Tags Peers
 | |
| // @Summary Retrieves all peers for the given interface
 | |
| // @Produce json
 | |
| // @Param device query string true "Device Name"
 | |
| // @Success 200 {object} []wireguard.Peer
 | |
| // @Failure 401 {object} ApiError
 | |
| // @Failure 403 {object} ApiError
 | |
| // @Failure 404 {object} ApiError
 | |
| // @Router /backend/peers [get]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) GetPeers(c *gin.Context) {
 | |
| 	deviceName := strings.ToLower(strings.TrimSpace(c.Query("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 query 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 [get]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) GetPeer(c *gin.Context) {
 | |
| 	pkey := c.Query("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 query 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 [post]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) PostPeer(c *gin.Context) {
 | |
| 	deviceName := strings.ToLower(strings.TrimSpace(c.Query("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 query 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 [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.Query("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 query 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 [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.Query("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 query 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 [delete]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) DeletePeer(c *gin.Context) {
 | |
| 	pkey := c.Query("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 query 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 [get]
 | |
| // @Security ApiBasicAuth
 | |
| func (s *ApiServer) GetDevice(c *gin.Context) {
 | |
| 	deviceName := strings.ToLower(strings.TrimSpace(c.Query("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 query 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 [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.Query("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 query 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 [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.Query("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)
 | |
| }
 | |
| 
 | |
| type PeerDeploymentInformation struct {
 | |
| 	PublicKey        string
 | |
| 	Identifier       string
 | |
| 	Device           string
 | |
| 	DeviceIdentifier string
 | |
| }
 | |
| 
 | |
| // GetPeerDeploymentInformation godoc
 | |
| // @Tags Provisioning
 | |
| // @Summary Retrieves all active peers for the given email address
 | |
| // @Produce json
 | |
| // @Param email query string true "Email Address"
 | |
| // @Success 200 {object} []PeerDeploymentInformation "All active WireGuard peers"
 | |
| // @Failure 401 {object} ApiError
 | |
| // @Failure 403 {object} ApiError
 | |
| // @Failure 404 {object} ApiError
 | |
| // @Router /provisioning/peers [get]
 | |
| // @Security GeneralBasicAuth
 | |
| func (s *ApiServer) GetPeerDeploymentInformation(c *gin.Context) {
 | |
| 	email := c.Query("email")
 | |
| 	if email == "" {
 | |
| 		c.JSON(http.StatusBadRequest, ApiError{Message: "email parameter must be specified"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Get authenticated user to check permissions
 | |
| 	username, _, _ := c.Request.BasicAuth()
 | |
| 	user := s.s.users.GetUser(username)
 | |
| 
 | |
| 	if !user.IsAdmin && user.Email != email {
 | |
| 		c.JSON(http.StatusForbidden, ApiError{Message: "not enough permissions to access this resource"})
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	peers := s.s.peers.GetPeersByMail(email)
 | |
| 	result := make([]PeerDeploymentInformation, 0, len(peers))
 | |
| 	for i := range peers {
 | |
| 		if peers[i].DeactivatedAt != nil {
 | |
| 			continue // skip deactivated peers
 | |
| 		}
 | |
| 
 | |
| 		device := s.s.peers.GetDevice(peers[i].DeviceName)
 | |
| 		if device.Type != wireguard.DeviceTypeServer {
 | |
| 			continue // Skip peers on non-server devices
 | |
| 		}
 | |
| 
 | |
| 		result = append(result, PeerDeploymentInformation{
 | |
| 			PublicKey:        peers[i].PublicKey,
 | |
| 			Identifier:       peers[i].Identifier,
 | |
| 			Device:           device.DeviceName,
 | |
| 			DeviceIdentifier: device.DisplayName,
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	c.JSON(http.StatusOK, result)
 | |
| }
 | |
| 
 | |
| // GetPeerDeploymentConfig godoc
 | |
| // @Tags Provisioning
 | |
| // @Summary Retrieves the peer config for the given public key
 | |
| // @Produce plain
 | |
| // @Param pkey query 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 [get]
 | |
| // @Security GeneralBasicAuth
 | |
| func (s *ApiServer) GetPeerDeploymentConfig(c *gin.Context) {
 | |
| 	pkey := c.Query("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/peers [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)
 | |
| }
 |