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:
h44z
2025-01-11 18:44:55 +01:00
committed by GitHub
parent ad267ed0a8
commit d596f578f6
53 changed files with 11028 additions and 274 deletions

View File

@@ -0,0 +1,109 @@
package backend
import (
"context"
"fmt"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
type InterfaceServiceInterfaceManagerRepo interface {
GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error)
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
CreateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, error)
UpdateInterface(ctx context.Context, in *domain.Interface) (*domain.Interface, []domain.Peer, error)
DeleteInterface(ctx context.Context, id domain.InterfaceIdentifier) error
}
type InterfaceService struct {
cfg *config.Config
interfaces InterfaceServiceInterfaceManagerRepo
users PeerServiceUserManagerRepo
}
func NewInterfaceService(cfg *config.Config, interfaces InterfaceServiceInterfaceManagerRepo) *InterfaceService {
return &InterfaceService{
cfg: cfg,
interfaces: interfaces,
}
}
func (s InterfaceService) GetAll(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, nil, err
}
interfaces, interfacePeers, err := s.interfaces.GetAllInterfacesAndPeers(ctx)
if err != nil {
return nil, nil, err
}
return interfaces, interfacePeers, nil
}
func (s InterfaceService) GetById(ctx context.Context, id domain.InterfaceIdentifier) (
*domain.Interface,
[]domain.Peer,
error,
) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, nil, err
}
interfaceData, interfacePeers, err := s.interfaces.GetInterfaceAndPeers(ctx, id)
if err != nil {
return nil, nil, err
}
return interfaceData, interfacePeers, nil
}
func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
createdInterface, err := s.interfaces.CreateInterface(ctx, iface)
if err != nil {
return nil, err
}
return createdInterface, nil
}
func (s InterfaceService) Update(ctx context.Context, id domain.InterfaceIdentifier, iface *domain.Interface) (
*domain.Interface,
[]domain.Peer,
error,
) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, nil, err
}
if iface.Identifier != id {
return nil, nil, fmt.Errorf("interface id mismatch: %s != %s: %w",
iface.Identifier, id, domain.ErrInvalidData)
}
updatedInterface, updatedPeers, err := s.interfaces.UpdateInterface(ctx, iface)
if err != nil {
return nil, nil, err
}
return updatedInterface, updatedPeers, nil
}
func (s InterfaceService) Delete(ctx context.Context, id domain.InterfaceIdentifier) error {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return err
}
err := s.interfaces.DeleteInterface(ctx, id)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,143 @@
package backend
import (
"context"
"errors"
"fmt"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
type PeerServicePeerManagerRepo interface {
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetUserPeers(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error)
GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Peer, error)
DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
}
type PeerServiceUserManagerRepo interface {
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
}
type PeerService struct {
cfg *config.Config
peers PeerServicePeerManagerRepo
users PeerServiceUserManagerRepo
}
func NewPeerService(
cfg *config.Config,
peers PeerServicePeerManagerRepo,
users PeerServiceUserManagerRepo,
) *PeerService {
return &PeerService{
cfg: cfg,
peers: peers,
users: users,
}
}
func (s PeerService) GetForInterface(ctx context.Context, id domain.InterfaceIdentifier) ([]domain.Peer, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
_, interfacePeers, err := s.peers.GetInterfaceAndPeers(ctx, id)
if err != nil {
return nil, err
}
return interfacePeers, nil
}
func (s PeerService) GetForUser(ctx context.Context, id domain.UserIdentifier) ([]domain.Peer, error) {
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
return nil, err
}
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
}
user, err := s.users.GetUser(ctx, id)
if err != nil {
return nil, err
}
userPeers, err := s.peers.GetUserPeers(ctx, user.Identifier)
if err != nil {
return nil, err
}
return userPeers, nil
}
func (s PeerService) GetById(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error) {
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
}
peer, err := s.peers.GetPeer(ctx, id)
if err != nil {
return nil, err
}
// Check if the user has access rights to the requested peer.
// If the peer is not linked to any user, access is granted only for admins.
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
return nil, err
}
return peer, nil
}
func (s PeerService) Create(ctx context.Context, peer *domain.Peer) (*domain.Peer, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
if peer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
return nil, fmt.Errorf("peer id mismatch: %s != %s: %w",
peer.Identifier, peer.Interface.PublicKey, domain.ErrInvalidData)
}
createdPeer, err := s.peers.CreatePeer(ctx, peer)
if err != nil {
return nil, err
}
return createdPeer, nil
}
func (s PeerService) Update(ctx context.Context, _ domain.PeerIdentifier, peer *domain.Peer) (
*domain.Peer,
error,
) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
updatedPeer, err := s.peers.UpdatePeer(ctx, peer)
if err != nil {
return nil, err
}
return updatedPeer, nil
}
func (s PeerService) Delete(ctx context.Context, id domain.PeerIdentifier) error {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return err
}
err := s.peers.DeletePeer(ctx, id)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,174 @@
package backend
import (
"context"
"fmt"
"io"
"github.com/h44z/wg-portal/internal/app/api/v1/models"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
type ProvisioningServiceUserManagerRepo interface {
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
}
type ProvisioningServicePeerManagerRepo interface {
GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
GetUserPeers(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
}
type ProvisioningServiceConfigFileManagerRepo interface {
GetPeerConfig(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
GetPeerConfigQrCode(ctx context.Context, id domain.PeerIdentifier) (io.Reader, error)
}
type ProvisioningService struct {
cfg *config.Config
users ProvisioningServiceUserManagerRepo
peers ProvisioningServicePeerManagerRepo
configFiles ProvisioningServiceConfigFileManagerRepo
}
func NewProvisioningService(
cfg *config.Config,
users ProvisioningServiceUserManagerRepo,
peers ProvisioningServicePeerManagerRepo,
configFiles ProvisioningServiceConfigFileManagerRepo,
) *ProvisioningService {
return &ProvisioningService{
cfg: cfg,
users: users,
peers: peers,
configFiles: configFiles,
}
}
func (p ProvisioningService) GetUserAndPeers(
ctx context.Context,
userId domain.UserIdentifier,
email string,
) (*domain.User, []domain.Peer, error) {
// first fetch user
var user *domain.User
switch {
case userId != "":
u, err := p.users.GetUser(ctx, userId)
if err != nil {
return nil, nil, err
}
user = u
case email != "":
u, err := p.users.GetUserByEmail(ctx, email)
if err != nil {
return nil, nil, err
}
user = u
default:
return nil, nil, fmt.Errorf("either UserId or Email must be set: %w", domain.ErrInvalidData)
}
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
return nil, nil, err
}
peers, err := p.peers.GetUserPeers(ctx, user.Identifier)
if err != nil {
return nil, nil, err
}
return user, peers, nil
}
func (p ProvisioningService) GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
peer, err := p.peers.GetPeer(ctx, peerId)
if err != nil {
return nil, err
}
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
return nil, err
}
peerCfgReader, err := p.configFiles.GetPeerConfig(ctx, peer.Identifier)
if err != nil {
return nil, err
}
peerCfgData, err := io.ReadAll(peerCfgReader)
if err != nil {
return nil, err
}
return peerCfgData, nil
}
func (p ProvisioningService) GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error) {
peer, err := p.peers.GetPeer(ctx, peerId)
if err != nil {
return nil, err
}
if err := domain.ValidateUserAccessRights(ctx, peer.UserIdentifier); err != nil {
return nil, err
}
peerCfgQrReader, err := p.configFiles.GetPeerConfigQrCode(ctx, peer.Identifier)
if err != nil {
return nil, err
}
peerCfgQrData, err := io.ReadAll(peerCfgQrReader)
if err != nil {
return nil, err
}
return peerCfgQrData, nil
}
func (p ProvisioningService) NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error) {
if req.UserIdentifier == "" {
req.UserIdentifier = string(domain.GetUserInfo(ctx).Id) // use authenticated user id if not set
}
// check permissions
if err := domain.ValidateUserAccessRights(ctx, domain.UserIdentifier(req.UserIdentifier)); err != nil {
return nil, err
}
if !p.cfg.Core.SelfProvisioningAllowed {
// only admins can create new peers if self-provisioning is disabled
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
}
// prepare new peer
peer, err := p.peers.PreparePeer(ctx, domain.InterfaceIdentifier(req.InterfaceIdentifier))
if err != nil {
return nil, fmt.Errorf("failed to prepare new peer: %w", err)
}
peer.UserIdentifier = domain.UserIdentifier(req.UserIdentifier) // overwrite context user id with the one from the request
if req.PublicKey != "" {
peer.Identifier = domain.PeerIdentifier(req.PublicKey)
peer.Interface.PublicKey = req.PublicKey
peer.Interface.PrivateKey = "" // clear private key if public key is set, WireGuard Portal does not know the private key in that case
}
if req.PresharedKey != "" {
peer.PresharedKey = domain.PreSharedKey(req.PresharedKey)
}
peer.GenerateDisplayName("API")
// save new peer
peer, err = p.peers.CreatePeer(ctx, peer)
if err != nil {
return nil, fmt.Errorf("failed to create new peer: %w", err)
}
return peer, nil
}

View File

@@ -0,0 +1,107 @@
package backend
import (
"context"
"errors"
"fmt"
"github.com/h44z/wg-portal/internal/config"
"github.com/h44z/wg-portal/internal/domain"
)
type UserManagerRepo interface {
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
GetAllUsers(ctx context.Context) ([]domain.User, error)
CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
DeleteUser(ctx context.Context, id domain.UserIdentifier) error
}
type UserService struct {
cfg *config.Config
users UserManagerRepo
}
func NewUserService(cfg *config.Config, users UserManagerRepo) *UserService {
return &UserService{
cfg: cfg,
users: users,
}
}
func (s UserService) GetAll(ctx context.Context) ([]domain.User, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
allUsers, err := s.users.GetAllUsers(ctx)
if err != nil {
return nil, err
}
return allUsers, nil
}
func (s UserService) GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
if err := domain.ValidateUserAccessRights(ctx, id); err != nil {
return nil, err
}
if s.cfg.Advanced.ApiAdminOnly && !domain.GetUserInfo(ctx).IsAdmin {
return nil, errors.Join(errors.New("only admins can access this endpoint"), domain.ErrNoPermission)
}
user, err := s.users.GetUser(ctx, id)
if err != nil {
return nil, err
}
return user, nil
}
func (s UserService) Create(ctx context.Context, user *domain.User) (*domain.User, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
createdUser, err := s.users.CreateUser(ctx, user)
if err != nil {
return nil, err
}
return createdUser, nil
}
func (s UserService) Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (
*domain.User,
error,
) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err
}
if id != user.Identifier {
return nil, fmt.Errorf("user id mismatch: %s != %s: %w", id, user.Identifier, domain.ErrInvalidData)
}
updatedUser, err := s.users.UpdateUser(ctx, user)
if err != nil {
return nil, err
}
return updatedUser, nil
}
func (s UserService) Delete(ctx context.Context, id domain.UserIdentifier) error {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return err
}
err := s.users.DeleteUser(ctx, id)
if err != nil {
return err
}
return nil
}

View 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(),
}
}

View 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)
}
}

View 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)
}
}

View 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))
}
}

View 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)
}
}

View 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
}

View File

@@ -0,0 +1,46 @@
package models
import (
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/domain"
)
type ConfigOption[T any] struct {
Value T `json:"Value"`
Overridable bool `json:"Overridable,omitempty"`
}
func NewConfigOption[T any](value T, overridable bool) ConfigOption[T] {
return ConfigOption[T]{
Value: value,
Overridable: overridable,
}
}
func ConfigOptionFromDomain[T any](opt domain.ConfigOption[T]) ConfigOption[T] {
return ConfigOption[T]{
Value: opt.Value,
Overridable: opt.Overridable,
}
}
func ConfigOptionToDomain[T any](opt ConfigOption[T]) domain.ConfigOption[T] {
return domain.ConfigOption[T]{
Value: opt.Value,
Overridable: opt.Overridable,
}
}
func StringSliceConfigOptionFromDomain(opt domain.ConfigOption[string]) ConfigOption[[]string] {
return ConfigOption[[]string]{
Value: internal.SliceString(opt.Value),
Overridable: opt.Overridable,
}
}
func StringSliceConfigOptionToDomain(opt ConfigOption[[]string]) domain.ConfigOption[string] {
return domain.ConfigOption[string]{
Value: internal.SliceToString(opt.Value),
Overridable: opt.Overridable,
}
}

View File

@@ -0,0 +1,8 @@
package models
// Error represents an error response.
type Error struct {
Code int `json:"Code"` // HTTP status code.
Message string `json:"Message"` // Error message.
Details string `json:"Details,omitempty"` // Additional error details.
}

View File

@@ -0,0 +1,201 @@
package models
import (
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/domain"
)
// Interface represents a WireGuard interface.
type Interface struct {
// Identifier is the unique identifier of the interface. It is always equal to the device name of the interface.
Identifier string `json:"Identifier" example:"wg0" binding:"required"`
// DisplayName is a nice display name / description for the interface.
DisplayName string `json:"DisplayName" binding:"omitempty,max=64" example:"My Interface"`
// Mode is the interface type, either 'server', 'client' or 'any'. The mode specifies how WireGuard Portal handles peers for this interface.
Mode string `json:"Mode" example:"server" binding:"required,oneof=server client any"`
// PrivateKey is the private key of the interface.
PrivateKey string `json:"PrivateKey" example:"gI6EdUSYvn8ugXOt8QQD6Yc+JyiZxIhp3GInSWRfWGE=" binding:"required,len=44"`
// PublicKey is the public key of the server interface. The public key is used by peers to connect to the server.
PublicKey string `json:"PublicKey" example:"HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=" binding:"required,len=44"`
// Disabled is a flag that specifies if the interface is enabled (up) or not (down). Disabled interfaces are not able to accept connections.
Disabled bool `json:"Disabled" example:"false"`
// DisabledReason is the reason why the interface has been disabled.
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the interface has been disabled."`
// SaveConfig is a flag that specifies if the configuration should be saved to the configuration file (wgX.conf in wg-quick format).
SaveConfig bool `json:"SaveConfig" example:"false"`
// ListenPort is the listening port, for example: 51820. The listening port is only required for server interfaces.
ListenPort int `json:"ListenPort" binding:"omitempty,min=1,max=65535" example:"51820"`
// Addresses is a list of IP addresses (in CIDR format) that are assigned to the interface.
Addresses []string `json:"Addresses" binding:"omitempty,dive,cidr" example:"10.11.12.1/24"`
// Dns is a list of DNS servers that should be set if the interface is up.
Dns []string `json:"Dns" binding:"omitempty,dive,ip" example:"1.1.1.1"`
// DnsSearch is the dns search option string that should be set if the interface is up, will be appended to Dns servers.
DnsSearch []string `json:"DnsSearch" binding:"omitempty,dive,fqdn" example:"wg.local"`
// Mtu is the device MTU of the interface.
Mtu int `json:"Mtu" binding:"omitempty,min=1,max=9000" example:"1420"`
// FirewallMark is an optional firewall mark which is used to handle interface traffic.
FirewallMark uint32 `json:"FirewallMark"`
// RoutingTable is an optional routing table which is used to route interface traffic.
RoutingTable string `json:"RoutingTable"`
// PreUp is an optional action that is executed before the device is up.
PreUp string `json:"PreUp" example:"echo 'Interface is up'"`
// PostUp is an optional action that is executed after the device is up.
PostUp string `json:"PostUp" example:"iptables -A FORWARD -i %i -j ACCEPT"`
// PreDown is an optional action that is executed before the device is down.
PreDown string `json:"PreDown" example:"iptables -D FORWARD -i %i -j ACCEPT"`
// PostDown is an optional action that is executed after the device is down.
PostDown string `json:"PostDown" example:"echo 'Interface is down'"`
// PeerDefNetwork specifies the default subnets from which new peers will get their IP addresses. The subnet is specified in CIDR format.
PeerDefNetwork []string `json:"PeerDefNetwork" example:"10.11.12.0/24"`
// PeerDefDns specifies the default dns servers for a new peer.
PeerDefDns []string `json:"PeerDefDns" example:"8.8.8.8"`
// PeerDefDnsSearch specifies the default dns search options for a new peer.
PeerDefDnsSearch []string `json:"PeerDefDnsSearch" example:"wg.local"`
// PeerDefEndpoint specifies the default endpoint for a new peer.
PeerDefEndpoint string `json:"PeerDefEndpoint" example:"wg.example.com:51820"`
// PeerDefAllowedIPs specifies the default allowed IP addresses for a new peer.
PeerDefAllowedIPs []string `json:"PeerDefAllowedIPs" example:"10.11.12.0/24"`
// PeerDefMtu specifies the default device MTU for a new peer.
PeerDefMtu int `json:"PeerDefMtu" example:"1420"`
// PeerDefPersistentKeepalive specifies the default persistent keep-alive value in seconds for a new peer.
PeerDefPersistentKeepalive int `json:"PeerDefPersistentKeepalive" example:"25"`
// PeerDefFirewallMark specifies the default firewall mark for a new peer.
PeerDefFirewallMark uint32 `json:"PeerDefFirewallMark"`
// PeerDefRoutingTable specifies the default routing table for a new peer.
PeerDefRoutingTable string `json:"PeerDefRoutingTable"`
// PeerDefPreUp specifies the default action that is executed before the device is up for a new peer.
PeerDefPreUp string `json:"PeerDefPreUp"`
// PeerDefPostUp specifies the default action that is executed after the device is up for a new peer.
PeerDefPostUp string `json:"PeerDefPostUp"`
// PeerDefPreDown specifies the default action that is executed before the device is down for a new peer.
PeerDefPreDown string `json:"PeerDefPreDown"`
// PeerDefPostDown specifies the default action that is executed after the device is down for a new peer.
PeerDefPostDown string `json:"PeerDefPostDown"`
// Calculated values
// EnabledPeers is the number of enabled peers for this interface. Only enabled peers are able to connect.
EnabledPeers int `json:"EnabledPeers" readonly:"true"`
// TotalPeers is the total number of peers for this interface.
TotalPeers int `json:"TotalPeers" readonly:"true"`
}
func NewInterface(src *domain.Interface, peers []domain.Peer) *Interface {
iface := &Interface{
Identifier: string(src.Identifier),
DisplayName: src.DisplayName,
Mode: string(src.Type),
PrivateKey: src.PrivateKey,
PublicKey: src.PublicKey,
Disabled: src.IsDisabled(),
DisabledReason: src.DisabledReason,
SaveConfig: src.SaveConfig,
ListenPort: src.ListenPort,
Addresses: domain.CidrsToStringSlice(src.Addresses),
Dns: internal.SliceString(src.DnsStr),
DnsSearch: internal.SliceString(src.DnsSearchStr),
Mtu: src.Mtu,
FirewallMark: src.FirewallMark,
RoutingTable: src.RoutingTable,
PreUp: src.PreUp,
PostUp: src.PostUp,
PreDown: src.PreDown,
PostDown: src.PostDown,
PeerDefNetwork: internal.SliceString(src.PeerDefNetworkStr),
PeerDefDns: internal.SliceString(src.PeerDefDnsStr),
PeerDefDnsSearch: internal.SliceString(src.PeerDefDnsSearchStr),
PeerDefEndpoint: src.PeerDefEndpoint,
PeerDefAllowedIPs: internal.SliceString(src.PeerDefAllowedIPsStr),
PeerDefMtu: src.PeerDefMtu,
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
PeerDefFirewallMark: src.PeerDefFirewallMark,
PeerDefRoutingTable: src.PeerDefRoutingTable,
PeerDefPreUp: src.PeerDefPreUp,
PeerDefPostUp: src.PeerDefPostUp,
PeerDefPreDown: src.PeerDefPreDown,
PeerDefPostDown: src.PeerDefPostDown,
EnabledPeers: 0,
TotalPeers: 0,
}
if len(peers) > 0 {
iface.TotalPeers = len(peers)
activePeers := 0
for _, peer := range peers {
if !peer.IsDisabled() {
activePeers++
}
}
iface.EnabledPeers = activePeers
}
return iface
}
func NewInterfaces(src []domain.Interface, srcPeers [][]domain.Peer) []Interface {
results := make([]Interface, len(src))
for i := range src {
results[i] = *NewInterface(&src[i], srcPeers[i])
}
return results
}
func NewDomainInterface(src *Interface) *domain.Interface {
now := time.Now()
cidrs, _ := domain.CidrsFromArray(src.Addresses)
res := &domain.Interface{
BaseModel: domain.BaseModel{},
Identifier: domain.InterfaceIdentifier(src.Identifier),
KeyPair: domain.KeyPair{
PrivateKey: src.PrivateKey,
PublicKey: src.PublicKey,
},
ListenPort: src.ListenPort,
Addresses: cidrs,
DnsStr: internal.SliceToString(src.Dns),
DnsSearchStr: internal.SliceToString(src.DnsSearch),
Mtu: src.Mtu,
FirewallMark: src.FirewallMark,
RoutingTable: src.RoutingTable,
PreUp: src.PreUp,
PostUp: src.PostUp,
PreDown: src.PreDown,
PostDown: src.PostDown,
SaveConfig: src.SaveConfig,
DisplayName: src.DisplayName,
Type: domain.InterfaceType(src.Mode),
DriverType: "", // currently unused
Disabled: nil, // set below
DisabledReason: src.DisabledReason,
PeerDefNetworkStr: internal.SliceToString(src.PeerDefNetwork),
PeerDefDnsStr: internal.SliceToString(src.PeerDefDns),
PeerDefDnsSearchStr: internal.SliceToString(src.PeerDefDnsSearch),
PeerDefEndpoint: src.PeerDefEndpoint,
PeerDefAllowedIPsStr: internal.SliceToString(src.PeerDefAllowedIPs),
PeerDefMtu: src.PeerDefMtu,
PeerDefPersistentKeepalive: src.PeerDefPersistentKeepalive,
PeerDefFirewallMark: src.PeerDefFirewallMark,
PeerDefRoutingTable: src.PeerDefRoutingTable,
PeerDefPreUp: src.PeerDefPreUp,
PeerDefPostUp: src.PeerDefPostUp,
PeerDefPreDown: src.PeerDefPreDown,
PeerDefPostDown: src.PeerDefPostDown,
}
if src.Disabled {
res.Disabled = &now
}
return res
}

View File

@@ -0,0 +1,195 @@
package models
import (
"time"
"github.com/h44z/wg-portal/internal"
"github.com/h44z/wg-portal/internal/domain"
)
const ExpiryDateTimeLayout = "\"2006-01-02\""
type ExpiryDate struct {
*time.Time
}
// UnmarshalJSON will unmarshal using 2006-01-02 layout
func (d *ExpiryDate) UnmarshalJSON(b []byte) error {
if len(b) == 0 || string(b) == "null" || string(b) == "\"\"" {
return nil
}
parsed, err := time.Parse(ExpiryDateTimeLayout, string(b))
if err != nil {
return err
}
if !parsed.IsZero() {
d.Time = &parsed
}
return nil
}
// MarshalJSON will marshal using 2006-01-02 layout
func (d *ExpiryDate) MarshalJSON() ([]byte, error) {
if d == nil || d.Time == nil {
return []byte("null"), nil
}
s := d.Format(ExpiryDateTimeLayout)
return []byte(s), nil
}
// Peer represents a WireGuard peer entry.
type Peer struct {
// Identifier is the unique identifier of the peer. It is always equal to the public key of the peer.
Identifier string `json:"Identifier" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"required,len=44"`
// DisplayName is a nice display name / description for the peer.
DisplayName string `json:"DisplayName" example:"My Peer" binding:"omitempty,max=64"`
// UserIdentifier is the identifier of the user that owns the peer.
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
// InterfaceIdentifier is the identifier of the interface the peer is linked to.
InterfaceIdentifier string `json:"InterfaceIdentifier" binding:"required" example:"wg0"`
// Disabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
Disabled bool `json:"Disabled" example:"false"`
// DisabledReason is the reason why the peer has been disabled.
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:"This is a reason why the peer has been disabled."`
// ExpiresAt is the expiry date of the peer in YYYY-MM-DD format. An expired peer is not able to connect.
ExpiresAt ExpiryDate `json:"ExpiresAt,omitempty" binding:"omitempty,datetime=2006-01-02"`
// Notes is a note field for peers.
Notes string `json:"Notes" example:"This is a note for the peer."`
// Endpoint is the endpoint address of the peer.
Endpoint ConfigOption[string] `json:"Endpoint"`
// EndpointPublicKey is the endpoint public key.
EndpointPublicKey ConfigOption[string] `json:"EndpointPublicKey"`
// AllowedIPs is a list of allowed IP subnets for the peer.
AllowedIPs ConfigOption[[]string] `json:"AllowedIPs"`
// ExtraAllowedIPs is a list of additional allowed IP subnets for the peer. These allowed IP subnets are added on the server side.
ExtraAllowedIPs []string `json:"ExtraAllowedIPs"`
// PresharedKey is the optional pre-shared Key of the peer.
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
// PersistentKeepalive is the optional persistent keep-alive interval in seconds.
PersistentKeepalive ConfigOption[int] `json:"PersistentKeepalive" binding:"omitempty,gte=0"`
// PrivateKey is the private Key of the peer.
PrivateKey string `json:"PrivateKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"required,len=44"`
// PublicKey is the public Key of the server peer.
PublicKey string `json:"PublicKey" example:"TrMvSoP4jYQlY6RIzBgbssQqY3vxI2Pi+y71lOWWXX0=" binding:"omitempty,len=44"`
// Mode is the peer interface type (server, client, any).
Mode string `json:"Mode" example:"client" binding:"omitempty,oneof=server client any"`
// Addresses is a list of IP addresses in CIDR format (both IPv4 and IPv6) for the peer.
Addresses []string `json:"Addresses" example:"10.11.12.2/24" binding:"omitempty,dive,cidr"`
// CheckAliveAddress is an optional ip address or DNS name that is used for ping checks.
CheckAliveAddress string `json:"CheckAliveAddress" binding:"omitempty,ip|fqdn" example:"1.1.1.1"`
// Dns is a list of DNS servers that should be set if the peer interface is up.
Dns ConfigOption[[]string] `json:"Dns"`
// DnsSearch is the dns search option string that should be set if the peer interface is up, will be appended to Dns servers.
DnsSearch ConfigOption[[]string] `json:"DnsSearch"`
// Mtu is the device MTU of the peer.
Mtu ConfigOption[int] `json:"Mtu"`
// FirewallMark is an optional firewall mark which is used to handle peer traffic.
FirewallMark ConfigOption[uint32] `json:"FirewallMark"`
// RoutingTable is an optional routing table which is used to route peer traffic.
RoutingTable ConfigOption[string] `json:"RoutingTable"`
// PreUp is an optional action that is executed before the device is up.
PreUp ConfigOption[string] `json:"PreUp"`
// PostUp is an optional action that is executed after the device is up.
PostUp ConfigOption[string] `json:"PostUp"`
// PreDown is an optional action that is executed before the device is down.
PreDown ConfigOption[string] `json:"PreDown"`
// PostDown is an optional action that is executed after the device is down.
PostDown ConfigOption[string] `json:"PostDown"`
}
func NewPeer(src *domain.Peer) *Peer {
return &Peer{
Identifier: string(src.Identifier),
DisplayName: src.DisplayName,
UserIdentifier: string(src.UserIdentifier),
InterfaceIdentifier: string(src.InterfaceIdentifier),
Disabled: src.IsDisabled(),
DisabledReason: src.DisabledReason,
ExpiresAt: ExpiryDate{src.ExpiresAt},
Notes: src.Notes,
Endpoint: ConfigOptionFromDomain(src.Endpoint),
EndpointPublicKey: ConfigOptionFromDomain(src.EndpointPublicKey),
AllowedIPs: StringSliceConfigOptionFromDomain(src.AllowedIPsStr),
ExtraAllowedIPs: internal.SliceString(src.ExtraAllowedIPsStr),
PresharedKey: string(src.PresharedKey),
PersistentKeepalive: ConfigOptionFromDomain(src.PersistentKeepalive),
PrivateKey: src.Interface.PrivateKey,
PublicKey: src.Interface.PublicKey,
Mode: string(src.Interface.Type),
Addresses: domain.CidrsToStringSlice(src.Interface.Addresses),
CheckAliveAddress: src.Interface.CheckAliveAddress,
Dns: StringSliceConfigOptionFromDomain(src.Interface.DnsStr),
DnsSearch: StringSliceConfigOptionFromDomain(src.Interface.DnsSearchStr),
Mtu: ConfigOptionFromDomain(src.Interface.Mtu),
FirewallMark: ConfigOptionFromDomain(src.Interface.FirewallMark),
RoutingTable: ConfigOptionFromDomain(src.Interface.RoutingTable),
PreUp: ConfigOptionFromDomain(src.Interface.PreUp),
PostUp: ConfigOptionFromDomain(src.Interface.PostUp),
PreDown: ConfigOptionFromDomain(src.Interface.PreDown),
PostDown: ConfigOptionFromDomain(src.Interface.PostDown),
}
}
func NewPeers(src []domain.Peer) []Peer {
results := make([]Peer, len(src))
for i := range src {
results[i] = *NewPeer(&src[i])
}
return results
}
func NewDomainPeer(src *Peer) *domain.Peer {
now := time.Now()
cidrs, _ := domain.CidrsFromArray(src.Addresses)
res := &domain.Peer{
BaseModel: domain.BaseModel{},
Endpoint: ConfigOptionToDomain(src.Endpoint),
EndpointPublicKey: ConfigOptionToDomain(src.EndpointPublicKey),
AllowedIPsStr: StringSliceConfigOptionToDomain(src.AllowedIPs),
ExtraAllowedIPsStr: internal.SliceToString(src.ExtraAllowedIPs),
PresharedKey: domain.PreSharedKey(src.PresharedKey),
PersistentKeepalive: ConfigOptionToDomain(src.PersistentKeepalive),
DisplayName: src.DisplayName,
Identifier: domain.PeerIdentifier(src.Identifier),
UserIdentifier: domain.UserIdentifier(src.UserIdentifier),
InterfaceIdentifier: domain.InterfaceIdentifier(src.InterfaceIdentifier),
Disabled: nil, // set below
DisabledReason: src.DisabledReason,
ExpiresAt: src.ExpiresAt.Time,
Notes: src.Notes,
Interface: domain.PeerInterfaceConfig{
KeyPair: domain.KeyPair{
PrivateKey: src.PrivateKey,
PublicKey: src.PublicKey,
},
Type: domain.InterfaceType(src.Mode),
Addresses: cidrs,
CheckAliveAddress: src.CheckAliveAddress,
DnsStr: StringSliceConfigOptionToDomain(src.Dns),
DnsSearchStr: StringSliceConfigOptionToDomain(src.DnsSearch),
Mtu: ConfigOptionToDomain(src.Mtu),
FirewallMark: ConfigOptionToDomain(src.FirewallMark),
RoutingTable: ConfigOptionToDomain(src.RoutingTable),
PreUp: ConfigOptionToDomain(src.PreUp),
PostUp: ConfigOptionToDomain(src.PostUp),
PreDown: ConfigOptionToDomain(src.PreDown),
PostDown: ConfigOptionToDomain(src.PostDown),
},
}
if src.Disabled {
res.Disabled = &now
}
return res
}

View File

@@ -0,0 +1,75 @@
package models
import "github.com/h44z/wg-portal/internal/domain"
// UserInformation represents the information about a user and its linked peers.
type UserInformation struct {
// UserIdentifier is the unique identifier of the user.
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
// PeerCount is the number of peers linked to the user.
PeerCount int `json:"PeerCount" example:"2"`
// Peers is a list of peers linked to the user.
Peers []UserInformationPeer `json:"Peers"`
}
// UserInformationPeer represents the information about a peer.
type UserInformationPeer struct {
// Identifier is the unique identifier of the peer. It equals the public key of the peer.
Identifier string `json:"Identifier" example:"peer-1234567"`
// DisplayName is a user-defined description of the peer.
DisplayName string `json:"DisplayName" example:"My iPhone"`
// IPAddresses is a list of IP addresses in CIDR format assigned to the peer.
IpAddresses []string `json:"IpAddresses" example:"10.11.12.2/24"`
// IsDisabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.
IsDisabled bool `json:"IsDisabled,omitempty" example:"true"`
// InterfaceIdentifier is the unique identifier of the WireGuard Portal device the peer is connected to.
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0"`
}
func NewUserInformation(user *domain.User, peers []domain.Peer) *UserInformation {
if user == nil {
return &UserInformation{}
}
ui := &UserInformation{
UserIdentifier: string(user.Identifier),
PeerCount: len(peers),
}
for _, peer := range peers {
ui.Peers = append(ui.Peers, NewUserInformationPeer(peer))
}
if len(ui.Peers) == 0 {
ui.Peers = []UserInformationPeer{} // Ensure that the JSON output is an empty array instead of null.
}
return ui
}
func NewUserInformationPeer(peer domain.Peer) UserInformationPeer {
up := UserInformationPeer{
Identifier: string(peer.Identifier),
DisplayName: peer.DisplayName,
IpAddresses: domain.CidrsToStringSlice(peer.Interface.Addresses),
IsDisabled: peer.IsDisabled(),
InterfaceIdentifier: string(peer.InterfaceIdentifier),
}
return up
}
// ProvisioningRequest represents a request to provision a new peer.
type ProvisioningRequest struct {
// InterfaceIdentifier is the identifier of the WireGuard interface the peer should be linked to.
InterfaceIdentifier string `json:"InterfaceIdentifier" example:"wg0" binding:"required"`
// UserIdentifier is the identifier of the user the peer should be linked to.
// If no user identifier is set, the authenticated user is used.
UserIdentifier string `json:"UserIdentifier" example:"uid-1234567"`
// PublicKey is the optional public key of the peer. If no public key is set, a new key pair is generated.
PublicKey string `json:"PublicKey" example:"xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=" binding:"omitempty,len=44"`
// PresharedKey is the optional pre-shared key of the peer. If no pre-shared key is set, a new key is generated.
PresharedKey string `json:"PresharedKey" example:"yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk=" binding:"omitempty,len=44"`
}

View File

@@ -0,0 +1,125 @@
package models
import (
"time"
"github.com/h44z/wg-portal/internal/domain"
)
// User represents a user in the system.
type User struct {
// The unique identifier of the user.
Identifier string `json:"Identifier" binding:"required,max=64" example:"uid-1234567"`
// The email address of the user. This field is optional.
Email string `json:"Email" binding:"omitempty,email" example:"test@test.com"`
// The source of the user. This field is optional.
Source string `json:"Source" binding:"oneof=db" example:"db"`
// The name of the authentication provider. This field is read-only.
ProviderName string `json:"ProviderName,omitempty" readonly:"true" example:""`
// If this field is set, the user is an admin.
IsAdmin bool `json:"IsAdmin" binding:"required" example:"false"`
// The first name of the user. This field is optional.
Firstname string `json:"Firstname" example:"Max"`
// The last name of the user. This field is optional.
Lastname string `json:"Lastname" example:"Muster"`
// The phone number of the user. This field is optional.
Phone string `json:"Phone" example:"+1234546789"`
// The department of the user. This field is optional.
Department string `json:"Department" example:"Software Development"`
// Additional notes about the user. This field is optional.
Notes string `json:"Notes" example:"some sample notes"`
// The password of the user. This field is never populated on read operations.
Password string `json:"Password,omitempty" binding:"omitempty,min=16,max=64" example:""`
// If this field is set, the user is disabled.
Disabled bool `json:"Disabled" example:"false"`
// The reason why the user has been disabled.
DisabledReason string `json:"DisabledReason" binding:"required_if=Disabled true" example:""`
// If this field is set, the user is locked and thus unable to log in to WireGuard Portal.
Locked bool `json:"Locked" example:"false"`
// The reason why the user has been locked.
LockedReason string `json:"LockedReason" binding:"required_if=Locked true" example:""`
// The API token of the user. This field is never populated on bulk read operations.
ApiToken string `json:"ApiToken,omitempty" binding:"omitempty,min=32,max=64" example:""`
// If this field is set, the user is allowed to use the RESTful API. This field is read-only.
ApiEnabled bool `json:"ApiEnabled" readonly:"true" example:"false"`
// The number of peers linked to the user. This field is read-only.
PeerCount int `json:"PeerCount" readonly:"true" example:"2"`
}
func NewUser(src *domain.User, exposeCredentials bool) *User {
u := &User{
Identifier: string(src.Identifier),
Email: src.Email,
Source: string(src.Source),
ProviderName: src.ProviderName,
IsAdmin: src.IsAdmin,
Firstname: src.Firstname,
Lastname: src.Lastname,
Phone: src.Phone,
Department: src.Department,
Notes: src.Notes,
Password: "", // never fill password
Disabled: src.IsDisabled(),
DisabledReason: src.DisabledReason,
Locked: src.IsLocked(),
LockedReason: src.LockedReason,
ApiToken: "", // by default, do not expose API token
ApiEnabled: src.IsApiEnabled(),
PeerCount: src.LinkedPeerCount,
}
if exposeCredentials {
u.ApiToken = src.ApiToken
}
return u
}
func NewUsers(src []domain.User) []User {
results := make([]User, len(src))
for i := range src {
results[i] = *NewUser(&src[i], false)
}
return results
}
func NewDomainUser(src *User) *domain.User {
now := time.Now()
res := &domain.User{
Identifier: domain.UserIdentifier(src.Identifier),
Email: src.Email,
Source: domain.UserSource(src.Source),
ProviderName: src.ProviderName,
IsAdmin: src.IsAdmin,
Firstname: src.Firstname,
Lastname: src.Lastname,
Phone: src.Phone,
Department: src.Department,
Notes: src.Notes,
Password: domain.PrivateString(src.Password),
Disabled: nil, // set below
DisabledReason: src.DisabledReason,
Locked: nil, // set below
LockedReason: src.LockedReason,
}
if src.ApiToken != "" {
res.ApiToken = src.ApiToken
res.ApiTokenCreated = &now
}
if src.Disabled {
res.Disabled = &now
}
if src.Locked {
res.Locked = &now
}
return res
}