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:
109
internal/app/api/v1/backend/interface_service.go
Normal file
109
internal/app/api/v1/backend/interface_service.go
Normal 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
|
||||
}
|
143
internal/app/api/v1/backend/peer_service.go
Normal file
143
internal/app/api/v1/backend/peer_service.go
Normal 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
|
||||
}
|
174
internal/app/api/v1/backend/provisioning_service.go
Normal file
174
internal/app/api/v1/backend/provisioning_service.go
Normal 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
|
||||
}
|
107
internal/app/api/v1/backend/user_service.go
Normal file
107
internal/app/api/v1/backend/user_service.go
Normal 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
|
||||
}
|
Reference in New Issue
Block a user