mirror of
https://github.com/h44z/wg-portal.git
synced 2025-09-14 06:51:15 +00:00
API - CRUD for peers, interfaces and users (#340)
Public REST API implementation to handle peers, interfaces and users. It also includes some simple provisioning endpoints. The Swagger API documentation is available under /api/v1/doc.html
This commit is contained in:
81
internal/app/api/v1/handlers/base.go
Normal file
81
internal/app/api/v1/handlers/base.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/core"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type Handler interface {
|
||||
GetName() string
|
||||
RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler)
|
||||
}
|
||||
|
||||
// To compile the API documentation use the
|
||||
// api_build_tool
|
||||
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
|
||||
|
||||
// @title WireGuard Portal Public API
|
||||
// @version 1.0
|
||||
// @description The WireGuard Portal REST API enables efficient management of WireGuard VPN configurations through a set of JSON-based endpoints.
|
||||
// @description It supports creating and editing peers, interfaces, and user profiles, while also providing role-based access control and auditing.
|
||||
// @description This API allows seamless integration with external tools or scripts for automated network configuration and administration.
|
||||
|
||||
// @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 BasicAuth
|
||||
|
||||
// @BasePath /api/v1
|
||||
// @query.collection.format multi
|
||||
|
||||
func NewRestApi(userSource UserSource, handlers ...Handler) core.ApiEndpointSetupFunc {
|
||||
authenticator := &authenticationHandler{
|
||||
userSource: userSource,
|
||||
}
|
||||
|
||||
return func() (core.ApiVersion, core.GroupSetupFn) {
|
||||
return "v1", func(group *gin.RouterGroup) {
|
||||
group.Use(cors.Default())
|
||||
|
||||
// Handler functions
|
||||
for _, h := range handlers {
|
||||
h.RegisterRoutes(group, authenticator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ParseServiceError(err error) (int, models.Error) {
|
||||
if err == nil {
|
||||
return 500, models.Error{
|
||||
Code: 500,
|
||||
Message: "unknown server error",
|
||||
}
|
||||
}
|
||||
|
||||
code := http.StatusInternalServerError
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrNotFound):
|
||||
code = http.StatusNotFound
|
||||
case errors.Is(err, domain.ErrNoPermission):
|
||||
code = http.StatusForbidden
|
||||
case errors.Is(err, domain.ErrDuplicateEntry):
|
||||
code = http.StatusConflict
|
||||
case errors.Is(err, domain.ErrInvalidData):
|
||||
code = http.StatusBadRequest
|
||||
}
|
||||
|
||||
return code, models.Error{
|
||||
Code: code,
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
220
internal/app/api/v1/handlers/endpoint_interface.go
Normal file
220
internal/app/api/v1/handlers/endpoint_interface.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type InterfaceEndpointInterfaceService interface {
|
||||
GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
|
||||
GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
|
||||
Create(context.Context, *domain.Interface) (*domain.Interface, error)
|
||||
Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
|
||||
Delete(context.Context, domain.InterfaceIdentifier) error
|
||||
}
|
||||
|
||||
type InterfaceEndpoint struct {
|
||||
interfaces InterfaceEndpointInterfaceService
|
||||
}
|
||||
|
||||
func NewInterfaceEndpoint(interfaceService InterfaceEndpointInterfaceService) *InterfaceEndpoint {
|
||||
return &InterfaceEndpoint{
|
||||
interfaces: interfaceService,
|
||||
}
|
||||
}
|
||||
|
||||
func (e InterfaceEndpoint) GetName() string {
|
||||
return "InterfaceEndpoint"
|
||||
}
|
||||
|
||||
func (e InterfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||
apiGroup := g.Group("/interface", authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleByIdGet())
|
||||
|
||||
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID interface_handleAllGet
|
||||
// @Tags Interfaces
|
||||
// @Summary Get all interface records.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []models.Interface
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/all [get]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleAllGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
allInterfaces, allPeersPerInterface, err := e.interfaces.GetAll(ctx)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewInterfaces(allInterfaces, allPeersPerInterface))
|
||||
}
|
||||
}
|
||||
|
||||
// handleByIdGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID interfaces_handleByIdGet
|
||||
// @Tags Interfaces
|
||||
// @Summary Get a specific interface record by its identifier.
|
||||
// @Param id path string true "The interface identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Interface
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/by-id/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
iface, interfacePeers, err := e.interfaces.GetById(ctx, domain.InterfaceIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewInterface(iface, interfacePeers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleCreatePost returns a gorm handler function.
|
||||
//
|
||||
// @ID interfaces_handleCreatePost
|
||||
// @Tags Interfaces
|
||||
// @Summary Create a new interface record.
|
||||
// @Param request body models.Interface true "The interface data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Interface
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 409 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/new [post]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
var iface models.Interface
|
||||
err := c.BindJSON(&iface)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newInterface, err := e.interfaces.Create(ctx, models.NewDomainInterface(&iface))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewInterface(newInterface, nil))
|
||||
}
|
||||
}
|
||||
|
||||
// handleUpdatePut returns a gorm handler function.
|
||||
//
|
||||
// @ID interfaces_handleUpdatePut
|
||||
// @Tags Interfaces
|
||||
// @Summary Update an interface record.
|
||||
// @Param id path string true "The interface identifier."
|
||||
// @Param request body models.Interface true "The interface data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Interface
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/by-id/{id} [put]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
var iface models.Interface
|
||||
err := c.BindJSON(&iface)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updatedInterface, updatedInterfacePeers, err := e.interfaces.Update(
|
||||
ctx,
|
||||
domain.InterfaceIdentifier(id),
|
||||
models.NewDomainInterface(&iface),
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewInterface(updatedInterface, updatedInterfacePeers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete returns a gorm handler function.
|
||||
//
|
||||
// @ID interfaces_handleDelete
|
||||
// @Tags Interfaces
|
||||
// @Summary Delete the interface record.
|
||||
// @Param id path string true "The interface identifier."
|
||||
// @Produce json
|
||||
// @Success 204 "No content if deletion was successful."
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /interface/by-id/{id} [delete]
|
||||
// @Security BasicAuth
|
||||
func (e InterfaceEndpoint) handleDelete() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
err := e.interfaces.Delete(ctx, domain.InterfaceIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
261
internal/app/api/v1/handlers/endpoint_peer.go
Normal file
261
internal/app/api/v1/handlers/endpoint_peer.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type PeerService interface {
|
||||
GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
|
||||
GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
|
||||
GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
|
||||
Create(context.Context, *domain.Peer) (*domain.Peer, error)
|
||||
Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
|
||||
Delete(context.Context, domain.PeerIdentifier) error
|
||||
}
|
||||
|
||||
type PeerEndpoint struct {
|
||||
peers PeerService
|
||||
}
|
||||
|
||||
func NewPeerEndpoint(peerService PeerService) *PeerEndpoint {
|
||||
return &PeerEndpoint{
|
||||
peers: peerService,
|
||||
}
|
||||
}
|
||||
|
||||
func (e PeerEndpoint) GetName() string {
|
||||
return "PeerEndpoint"
|
||||
}
|
||||
|
||||
func (e PeerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||
apiGroup := g.Group("/peer", authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/by-interface/:id", authenticator.LoggedIn(ScopeAdmin), e.handleAllForInterfaceGet())
|
||||
apiGroup.GET("/by-user/:id", authenticator.LoggedIn(), e.handleAllForUserGet())
|
||||
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||
|
||||
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||
}
|
||||
|
||||
// handleAllForInterfaceGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID peers_handleAllForInterfaceGet
|
||||
// @Tags Peers
|
||||
// @Summary Get all peer records for a given WireGuard interface.
|
||||
// @Param id path string true "The WireGuard interface identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} []models.Peer
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-interface/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleAllForInterfaceGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
|
||||
return
|
||||
}
|
||||
|
||||
interfacePeers, err := e.peers.GetForInterface(ctx, domain.InterfaceIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleAllForUserGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID peers_handleAllForUserGet
|
||||
// @Tags Peers
|
||||
// @Summary Get all peer records for a given user.
|
||||
// @Description Normal users can only access their own records. Admins can access all records.
|
||||
// @Param id path string true "The user identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} []models.Peer
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-user/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleAllForUserGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
interfacePeers, err := e.peers.GetForUser(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
|
||||
}
|
||||
}
|
||||
|
||||
// handleByIdGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID peers_handleByIdGet
|
||||
// @Tags Peers
|
||||
// @Summary Get a specific peer record by its identifier (public key).
|
||||
// @Description Normal users can only access their own records. Admins can access all records.
|
||||
// @Param id path string true "The peer identifier (public key)."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Peer
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-id/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
peer, err := e.peers.GetById(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||
}
|
||||
}
|
||||
|
||||
// handleCreatePost returns a gorm handler function.
|
||||
//
|
||||
// @ID peers_handleCreatePost
|
||||
// @Tags Peers
|
||||
// @Summary Create a new peer record.
|
||||
// @Description Only admins can create new records.
|
||||
// @Param request body models.Peer true "The peer data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Peer
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 409 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/new [post]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
var peer models.Peer
|
||||
err := c.BindJSON(&peer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newPeer, err := e.peers.Create(ctx, models.NewDomainPeer(&peer))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeer(newPeer))
|
||||
}
|
||||
}
|
||||
|
||||
// handleUpdatePut returns a gorm handler function.
|
||||
//
|
||||
// @ID peers_handleUpdatePut
|
||||
// @Tags Peers
|
||||
// @Summary Update a peer record.
|
||||
// @Description Only admins can update existing records.
|
||||
// @Param id path string true "The peer identifier."
|
||||
// @Param request body models.Peer true "The peer data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Peer
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-id/{id} [put]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
var peer models.Peer
|
||||
err := c.BindJSON(&peer)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updatedPeer, err := e.peers.Update(ctx, domain.PeerIdentifier(id), models.NewDomainPeer(&peer))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeer(updatedPeer))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete returns a gorm handler function.
|
||||
//
|
||||
// @ID peers_handleDelete
|
||||
// @Tags Peers
|
||||
// @Summary Delete the peer record.
|
||||
// @Param id path string true "The peer identifier."
|
||||
// @Produce json
|
||||
// @Success 204 "No content if deletion was successful."
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /peer/by-id/{id} [delete]
|
||||
// @Security BasicAuth
|
||||
func (e PeerEndpoint) handleDelete() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
err := e.peers.Delete(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
195
internal/app/api/v1/handlers/endpoint_provisioning.go
Normal file
195
internal/app/api/v1/handlers/endpoint_provisioning.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type ProvisioningEndpointProvisioningService interface {
|
||||
GetUserAndPeers(ctx context.Context, userId domain.UserIdentifier, email string) (
|
||||
*domain.User,
|
||||
[]domain.Peer,
|
||||
error,
|
||||
)
|
||||
GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||
GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
|
||||
NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error)
|
||||
}
|
||||
|
||||
type ProvisioningEndpoint struct {
|
||||
provisioning ProvisioningEndpointProvisioningService
|
||||
}
|
||||
|
||||
func NewProvisioningEndpoint(provisioning ProvisioningEndpointProvisioningService) *ProvisioningEndpoint {
|
||||
return &ProvisioningEndpoint{
|
||||
provisioning: provisioning,
|
||||
}
|
||||
}
|
||||
|
||||
func (e ProvisioningEndpoint) GetName() string {
|
||||
return "ProvisioningEndpoint"
|
||||
}
|
||||
|
||||
func (e ProvisioningEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||
apiGroup := g.Group("/provisioning", authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/data/user-info", authenticator.LoggedIn(), e.handleUserInfoGet())
|
||||
apiGroup.GET("/data/peer-config", authenticator.LoggedIn(), e.handlePeerConfigGet())
|
||||
apiGroup.GET("/data/peer-qr", authenticator.LoggedIn(), e.handlePeerQrGet())
|
||||
|
||||
apiGroup.POST("/new-peer", authenticator.LoggedIn(), e.handleNewPeerPost())
|
||||
}
|
||||
|
||||
// handleUserInfoGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID provisioning_handleUserInfoGet
|
||||
// @Tags Provisioning
|
||||
// @Summary Get information about all peer records for a given user.
|
||||
// @Description Normal users can only access their own record. Admins can access all records.
|
||||
// @Param UserId query string false "The user identifier that should be queried. If not set, the authenticated user is used."
|
||||
// @Param Email query string false "The email address that should be queried. If UserId is set, this is ignored."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.UserInformation
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /provisioning/data/user-info [get]
|
||||
// @Security BasicAuth
|
||||
func (e ProvisioningEndpoint) handleUserInfoGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := strings.TrimSpace(c.Query("UserId"))
|
||||
email := strings.TrimSpace(c.Query("Email"))
|
||||
|
||||
if id == "" && email == "" {
|
||||
id = string(domain.GetUserInfo(ctx).Id)
|
||||
}
|
||||
|
||||
user, peers, err := e.provisioning.GetUserAndPeers(ctx, domain.UserIdentifier(id), email)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUserInformation(user, peers))
|
||||
}
|
||||
}
|
||||
|
||||
// handlePeerConfigGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID provisioning_handlePeerConfigGet
|
||||
// @Tags Provisioning
|
||||
// @Summary Get the peer configuration in wg-quick format.
|
||||
// @Description Normal users can only access their own record. Admins can access all records.
|
||||
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||
// @Produce plain
|
||||
// @Produce json
|
||||
// @Success 200 {string} string "The WireGuard configuration file"
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /provisioning/data/peer-config [get]
|
||||
// @Security BasicAuth
|
||||
func (e ProvisioningEndpoint) handlePeerConfigGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := strings.TrimSpace(c.Query("PeerId"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
peerConfig, err := e.provisioning.GetPeerConfig(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "text/plain", peerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePeerQrGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID provisioning_handlePeerQrGet
|
||||
// @Tags Provisioning
|
||||
// @Summary Get the peer configuration as QR code.
|
||||
// @Description Normal users can only access their own record. Admins can access all records.
|
||||
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
|
||||
// @Produce png
|
||||
// @Produce json
|
||||
// @Success 200 {file} binary "The WireGuard configuration QR code"
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /provisioning/data/peer-qr [get]
|
||||
// @Security BasicAuth
|
||||
func (e ProvisioningEndpoint) handlePeerQrGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := strings.TrimSpace(c.Query("PeerId"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
|
||||
return
|
||||
}
|
||||
|
||||
peerConfigQrCode, err := e.provisioning.GetPeerQrPng(ctx, domain.PeerIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Data(http.StatusOK, "image/png", peerConfigQrCode)
|
||||
}
|
||||
}
|
||||
|
||||
// handleNewPeerPost returns a gorm Handler function.
|
||||
//
|
||||
// @ID provisioning_handleNewPeerPost
|
||||
// @Tags Provisioning
|
||||
// @Summary Create a new peer for the given interface and user.
|
||||
// @Description Normal users can only create new peers if self provisioning is allowed. Admins can always add new peers.
|
||||
// @Param request body models.ProvisioningRequest true "Provisioning request model."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.Peer
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /provisioning/new-peer [post]
|
||||
// @Security BasicAuth
|
||||
func (e ProvisioningEndpoint) handleNewPeerPost() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
var req models.ProvisioningRequest
|
||||
err := c.BindJSON(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
peer, err := e.provisioning.NewPeer(ctx, req)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewPeer(peer))
|
||||
}
|
||||
}
|
218
internal/app/api/v1/handlers/endpoint_user.go
Normal file
218
internal/app/api/v1/handlers/endpoint_user.go
Normal file
@@ -0,0 +1,218 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v1/models"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type UserService interface {
|
||||
GetAll(ctx context.Context) ([]domain.User, error)
|
||||
GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
Create(ctx context.Context, user *domain.User) (*domain.User, error)
|
||||
Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (*domain.User, error)
|
||||
Delete(ctx context.Context, id domain.UserIdentifier) error
|
||||
}
|
||||
|
||||
type UserEndpoint struct {
|
||||
users UserService
|
||||
}
|
||||
|
||||
func NewUserEndpoint(userService UserService) *UserEndpoint {
|
||||
return &UserEndpoint{
|
||||
users: userService,
|
||||
}
|
||||
}
|
||||
|
||||
func (e UserEndpoint) GetName() string {
|
||||
return "UserEndpoint"
|
||||
}
|
||||
|
||||
func (e UserEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
|
||||
apiGroup := g.Group("/user", authenticator.LoggedIn())
|
||||
|
||||
apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
|
||||
apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
|
||||
apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
|
||||
apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
|
||||
apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
|
||||
}
|
||||
|
||||
// handleAllGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleAllGet
|
||||
// @Tags Users
|
||||
// @Summary Get all user records.
|
||||
// @Produce json
|
||||
// @Success 200 {object} []models.User
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/all [get]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleAllGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
users, err := e.users.GetAll(ctx)
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUsers(users))
|
||||
}
|
||||
}
|
||||
|
||||
// handleByIdGet returns a gorm Handler function.
|
||||
//
|
||||
// @ID users_handleByIdGet
|
||||
// @Tags Users
|
||||
// @Summary Get a specific user record by its internal identifier.
|
||||
// @Description Normal users can only access their own record. Admins can access all records.
|
||||
// @Param id path string true "The user identifier."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/by-id/{id} [get]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleByIdGet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := e.users.GetById(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUser(user, true))
|
||||
}
|
||||
}
|
||||
|
||||
// handleCreatePost returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleCreatePost
|
||||
// @Tags Users
|
||||
// @Summary Create a new user record.
|
||||
// @Description Only admins can create new records.
|
||||
// @Param request body models.User true "The user data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 409 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/new [post]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleCreatePost() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
var user models.User
|
||||
err := c.BindJSON(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
newUser, err := e.users.Create(ctx, models.NewDomainUser(&user))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUser(newUser, true))
|
||||
}
|
||||
}
|
||||
|
||||
// handleUpdatePut returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleUpdatePut
|
||||
// @Tags Users
|
||||
// @Summary Update a user record.
|
||||
// @Description Only admins can update existing records.
|
||||
// @Param id path string true "The user identifier."
|
||||
// @Param request body models.User true "The user data."
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.User
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/by-id/{id} [put]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleUpdatePut() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
var user models.User
|
||||
err := c.BindJSON(&user)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
updateUser, err := e.users.Update(ctx, domain.UserIdentifier(id), models.NewDomainUser(&user))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.NewUser(updateUser, true))
|
||||
}
|
||||
}
|
||||
|
||||
// handleDelete returns a gorm handler function.
|
||||
//
|
||||
// @ID users_handleDelete
|
||||
// @Tags Users
|
||||
// @Summary Delete the user record.
|
||||
// @Param id path string true "The user identifier."
|
||||
// @Produce json
|
||||
// @Success 204 "No content if deletion was successful."
|
||||
// @Failure 400 {object} models.Error
|
||||
// @Failure 401 {object} models.Error
|
||||
// @Failure 403 {object} models.Error
|
||||
// @Failure 404 {object} models.Error
|
||||
// @Failure 500 {object} models.Error
|
||||
// @Router /user/by-id/{id} [delete]
|
||||
// @Security BasicAuth
|
||||
func (e UserEndpoint) handleDelete() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx := domain.SetUserInfoFromGin(c)
|
||||
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
|
||||
return
|
||||
}
|
||||
|
||||
err := e.users.Delete(ctx, domain.UserIdentifier(id))
|
||||
if err != nil {
|
||||
c.JSON(ParseServiceError(err))
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
}
|
92
internal/app/api/v1/handlers/middleware_authentication.go
Normal file
92
internal/app/api/v1/handlers/middleware_authentication.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/h44z/wg-portal/internal/app/api/v0/model"
|
||||
"github.com/h44z/wg-portal/internal/domain"
|
||||
)
|
||||
|
||||
type Scope string
|
||||
|
||||
const (
|
||||
ScopeAdmin Scope = "ADMIN" // Admin scope contains all other scopes
|
||||
)
|
||||
|
||||
type UserSource interface {
|
||||
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
|
||||
}
|
||||
|
||||
type authenticationHandler struct {
|
||||
userSource UserSource
|
||||
}
|
||||
|
||||
// LoggedIn checks if a user is logged in. If scopes are given, they are validated as well.
|
||||
func (h authenticationHandler) LoggedIn(scopes ...Scope) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
username, password, ok := c.Request.BasicAuth()
|
||||
if !ok || username == "" || password == "" {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "missing credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// check if user exists in DB
|
||||
|
||||
ctx := domain.SetUserInfo(c.Request.Context(), domain.SystemAdminContextUserInfo())
|
||||
user, err := h.userSource.GetUser(ctx, domain.UserIdentifier(username))
|
||||
if err != nil {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// validate API token
|
||||
if err := user.CheckApiToken(password); err != nil {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
if !UserHasScopes(user, scopes...) {
|
||||
// Abort the request with the appropriate error code
|
||||
c.Abort()
|
||||
c.JSON(http.StatusForbidden, model.Error{Code: http.StatusForbidden, Message: "not enough permissions"})
|
||||
return
|
||||
}
|
||||
|
||||
c.Set(domain.CtxUserInfo, &domain.ContextUserInfo{
|
||||
Id: user.Identifier,
|
||||
IsAdmin: user.IsAdmin,
|
||||
})
|
||||
|
||||
// Continue down the chain to Handler etc
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func UserHasScopes(user *domain.User, scopes ...Scope) bool {
|
||||
// No scopes give, so the check should succeed
|
||||
if len(scopes) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// check if user has admin scope
|
||||
if user.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if admin scope is required
|
||||
for _, scope := range scopes {
|
||||
if scope == ScopeAdmin {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
Reference in New Issue
Block a user