first provisioning endpoints

This commit is contained in:
Christoph Haas 2025-01-11 11:17:50 +01:00
parent e35dff6538
commit 38838eb4ce
10 changed files with 856 additions and 14 deletions

View File

@ -108,10 +108,18 @@ func main() {
apiV1BackendUsers := backendV1.NewUserService(cfg, userManager)
apiV1BackendPeers := backendV1.NewPeerService(cfg, wireGuardManager, userManager)
apiV1BackendInterfaces := backendV1.NewInterfaceService(cfg, wireGuardManager)
apiV1BackendProvisioning := backendV1.NewProvisioningService(cfg, userManager, wireGuardManager, cfgFileManager)
apiV1EndpointUsers := handlersV1.NewUserEndpoint(apiV1BackendUsers)
apiV1EndpointPeers := handlersV1.NewPeerEndpoint(apiV1BackendPeers)
apiV1EndpointInterfaces := handlersV1.NewInterfaceEndpoint(apiV1BackendInterfaces)
apiV1 := handlersV1.NewRestApi(userManager, apiV1EndpointUsers, apiV1EndpointPeers, apiV1EndpointInterfaces)
apiV1EndpointProvisioning := handlersV1.NewProvisioningEndpoint(apiV1BackendProvisioning)
apiV1 := handlersV1.NewRestApi(
userManager,
apiV1EndpointUsers,
apiV1EndpointPeers,
apiV1EndpointInterfaces,
apiV1EndpointProvisioning,
)
webSrv, err := core.NewServer(cfg, apiFrontend, apiV1)
internal.AssertNoError(err)

View File

@ -698,6 +698,30 @@ func (r *SqlRepo) GetUser(ctx context.Context, id domain.UserIdentifier) (*domai
return &user, nil
}
func (r *SqlRepo) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
var users []domain.User
err := r.db.WithContext(ctx).Where("email = ?", email).Find(&users).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
return nil, domain.ErrNotFound
}
if err != nil {
return nil, err
}
if len(users) == 0 {
return nil, domain.ErrNotFound
}
if len(users) > 1 {
return nil, fmt.Errorf("found multiple users with email %s: %w", email, domain.ErrNotUnique)
}
user := users[0]
return &user, nil
}
func (r *SqlRepo) GetAllUsers(ctx context.Context) ([]domain.User, error) {
var users []domain.User

View File

@ -665,6 +665,208 @@
}
}
},
"/provisioning/data/peer-config": {
"get": {
"security": [
{
"BasicAuth": []
}
],
"description": "Normal users can only access their own record. Admins can access all records.",
"produces": [
"text/plain",
"application/json"
],
"tags": [
"Provisioning"
],
"summary": "Get the peer configuration in wg-quick format.",
"operationId": "provisioning_handlePeerConfigGet",
"parameters": [
{
"type": "string",
"description": "The peer identifier (public key) that should be queried.",
"name": "PeerId",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "The WireGuard configuration file",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.Error"
}
}
}
}
},
"/provisioning/data/peer-qr": {
"get": {
"security": [
{
"BasicAuth": []
}
],
"description": "Normal users can only access their own record. Admins can access all records.",
"produces": [
"image/png",
"application/json"
],
"tags": [
"Provisioning"
],
"summary": "Get the peer configuration as QR code.",
"operationId": "provisioning_handlePeerQrGet",
"parameters": [
{
"type": "string",
"description": "The peer identifier (public key) that should be queried.",
"name": "PeerId",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "The WireGuard configuration QR code",
"schema": {
"type": "file"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.Error"
}
}
}
}
},
"/provisioning/data/user-info": {
"get": {
"security": [
{
"BasicAuth": []
}
],
"description": "Normal users can only access their own record. Admins can access all records.",
"produces": [
"application/json"
],
"tags": [
"Provisioning"
],
"summary": "Get information about all peer records for a given user.",
"operationId": "provisioning_handleUserInfoGet",
"parameters": [
{
"type": "string",
"description": "The user identifier that should be queried. If not set, the authenticated user is used.",
"name": "UserId",
"in": "query"
},
{
"type": "string",
"description": "The email address that should be queried. If UserId is set, this is ignored.",
"name": "Email",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/models.UserInformation"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"403": {
"description": "Forbidden",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/models.Error"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/models.Error"
}
}
}
}
},
"/user/all": {
"get": {
"security": [
@ -1568,6 +1770,60 @@
"example": "db"
}
}
},
"models.UserInformation": {
"type": "object",
"properties": {
"PeerCount": {
"type": "integer",
"example": 2
},
"Peers": {
"type": "array",
"items": {
"$ref": "#/definitions/models.UserInformationPeer"
}
},
"UserIdentifier": {
"type": "string",
"example": "uid-1234567"
}
}
},
"models.UserInformationPeer": {
"type": "object",
"properties": {
"DisplayName": {
"description": "DisplayName is a user-defined description of the peer.",
"type": "string",
"example": "My iPhone"
},
"Identifier": {
"description": "Identifier is the unique identifier of the peer. It equals the public key of the peer.",
"type": "string",
"example": "peer-1234567"
},
"InterfaceIdentifier": {
"description": "InterfaceIdentifier is the unique identifier of the WireGuard Portal device the peer is connected to.",
"type": "string",
"example": "wg0"
},
"IpAddresses": {
"description": "IPAddresses is a list of IP addresses in CIDR format assigned to the peer.",
"type": "array",
"items": {
"type": "string"
},
"example": [
"10.11.12.2/24"
]
},
"IsDisabled": {
"description": "IsDisabled is a flag that specifies if the peer is enabled or not. Disabled peers are not able to connect.",
"type": "boolean",
"example": true
}
}
}
},
"securityDefinitions": {

View File

@ -475,6 +475,49 @@ definitions:
- Identifier
- IsAdmin
type: object
models.UserInformation:
properties:
PeerCount:
example: 2
type: integer
Peers:
items:
$ref: '#/definitions/models.UserInformationPeer'
type: array
UserIdentifier:
example: uid-1234567
type: string
type: object
models.UserInformationPeer:
properties:
DisplayName:
description: DisplayName is a user-defined description of the peer.
example: My iPhone
type: string
Identifier:
description: Identifier is the unique identifier of the peer. It equals the
public key of the peer.
example: peer-1234567
type: string
InterfaceIdentifier:
description: InterfaceIdentifier is the unique identifier of the WireGuard
Portal device the peer is connected to.
example: wg0
type: string
IpAddresses:
description: IPAddresses is a list of IP addresses in CIDR format assigned
to the peer.
example:
- 10.11.12.2/24
items:
type: string
type: array
IsDisabled:
description: IsDisabled is a flag that specifies if the peer is enabled or
not. Disabled peers are not able to connect.
example: true
type: boolean
type: object
info:
contact:
name: WireGuard Portal Project
@ -908,6 +951,142 @@ paths:
summary: Create a new peer record.
tags:
- Peers
/provisioning/data/peer-config:
get:
description: Normal users can only access their own record. Admins can access
all records.
operationId: provisioning_handlePeerConfigGet
parameters:
- description: The peer identifier (public key) that should be queried.
in: query
name: PeerId
required: true
type: string
produces:
- text/plain
- application/json
responses:
"200":
description: The WireGuard configuration file
schema:
type: string
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.Error'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Get the peer configuration in wg-quick format.
tags:
- Provisioning
/provisioning/data/peer-qr:
get:
description: Normal users can only access their own record. Admins can access
all records.
operationId: provisioning_handlePeerQrGet
parameters:
- description: The peer identifier (public key) that should be queried.
in: query
name: PeerId
required: true
type: string
produces:
- image/png
- application/json
responses:
"200":
description: The WireGuard configuration QR code
schema:
type: file
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.Error'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Get the peer configuration as QR code.
tags:
- Provisioning
/provisioning/data/user-info:
get:
description: Normal users can only access their own record. Admins can access
all records.
operationId: provisioning_handleUserInfoGet
parameters:
- description: The user identifier that should be queried. If not set, the authenticated
user is used.
in: query
name: UserId
type: string
- description: The email address that should be queried. If UserId is set, this
is ignored.
in: query
name: Email
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/models.UserInformation'
"400":
description: Bad Request
schema:
$ref: '#/definitions/models.Error'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/models.Error'
"403":
description: Forbidden
schema:
$ref: '#/definitions/models.Error'
"404":
description: Not Found
schema:
$ref: '#/definitions/models.Error'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/models.Error'
security:
- BasicAuth: []
summary: Get information about all peer records for a given user.
tags:
- Provisioning
/user/all:
get:
operationId: users_handleAllGet

View File

@ -1,12 +1,13 @@
package handlers
import (
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/h44z/wg-portal/internal/app"
"github.com/h44z/wg-portal/internal/app/api/v0/model"
"github.com/h44z/wg-portal/internal/domain"
"io"
"net/http"
)
type peerEndpoint struct {
@ -57,7 +58,8 @@ func (e peerEndpoint) handleAllGet() gin.HandlerFunc {
_, peers, err := e.app.GetInterfaceAndPeers(ctx, domain.InterfaceIdentifier(interfaceId))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
@ -88,7 +90,8 @@ func (e peerEndpoint) handleSingleGet() gin.HandlerFunc {
peer, err := e.app.GetPeer(ctx, domain.PeerIdentifier(peerId))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
@ -119,7 +122,8 @@ func (e peerEndpoint) handlePrepareGet() gin.HandlerFunc {
peer, err := e.app.PreparePeer(ctx, domain.InterfaceIdentifier(interfaceId))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
@ -163,7 +167,8 @@ func (e peerEndpoint) handleCreatePost() gin.HandlerFunc {
newPeer, err := e.app.CreatePeer(ctx, model.NewDomainPeer(&p))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
@ -200,9 +205,11 @@ func (e peerEndpoint) handleCreateMultiplePost() gin.HandlerFunc {
return
}
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId), model.NewDomainPeerCreationRequest(&req))
newPeers, err := e.app.CreateMultiplePeers(ctx, domain.InterfaceIdentifier(interfaceId),
model.NewDomainPeerCreationRequest(&req))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
@ -246,7 +253,8 @@ func (e peerEndpoint) handleUpdatePut() gin.HandlerFunc {
updatedPeer, err := e.app.UpdatePeer(ctx, model.NewDomainPeer(&p))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
@ -277,7 +285,8 @@ func (e peerEndpoint) handleDelete() gin.HandlerFunc {
err := e.app.DeletePeer(ctx, domain.PeerIdentifier(id))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
@ -333,9 +342,10 @@ func (e peerEndpoint) handleConfigGet() gin.HandlerFunc {
// @ID peers_handleQrCodeGet
// @Tags Peer
// @Summary Get peer configuration as qr code.
// @Produce png
// @Produce json
// @Param id path string true "The peer identifier"
// @Success 200 {object} string
// @Success 200 {object} file
// @Failure 400 {object} model.Error
// @Failure 500 {object} model.Error
// @Router /peer/config-qr/{id} [get]
@ -403,7 +413,8 @@ func (e peerEndpoint) handleEmailPost() gin.HandlerFunc {
}
err = e.app.SendPeerEmail(ctx, req.LinkOnly, peerIds...)
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}
@ -434,7 +445,8 @@ func (e peerEndpoint) handleStatsGet() gin.HandlerFunc {
stats, err := e.app.GetPeerStats(ctx, domain.InterfaceIdentifier(interfaceId))
if err != nil {
c.JSON(http.StatusInternalServerError, model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
c.JSON(http.StatusInternalServerError,
model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
return
}

View File

@ -0,0 +1,130 @@
package backend
import (
"context"
"fmt"
"io"
"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)
}
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
}

View File

@ -0,0 +1,155 @@
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)
}
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())
}
// 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)
}
}

View File

@ -0,0 +1,58 @@
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 string `json:"UserIdentifier" example:"uid-1234567"`
PeerCount int `json:"PeerCount" example:"2"`
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
}

View File

@ -2,11 +2,13 @@ package users
import (
"context"
"github.com/h44z/wg-portal/internal/domain"
)
type UserDatabaseRepo interface {
GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
GetUserByEmail(ctx context.Context, email string) (*domain.User, error)
GetAllUsers(ctx context.Context) ([]domain.User, error)
FindUsers(ctx context.Context, search string) ([]domain.User, error)
SaveUser(ctx context.Context, id domain.UserIdentifier, updateFunc func(u *domain.User) (*domain.User, error)) error

View File

@ -111,6 +111,24 @@ func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain
return user, nil
}
func (m Manager) GetUserByEmail(ctx context.Context, email string) (*domain.User, error) {
user, err := m.users.GetUserByEmail(ctx, email)
if err != nil {
return nil, fmt.Errorf("unable to load user for email %s: %w", email, err)
}
if err := domain.ValidateUserAccessRights(ctx, user.Identifier); err != nil {
return nil, err
}
peers, _ := m.peers.GetUserPeers(ctx, user.Identifier) // ignore error, list will be empty in error case
user.LinkedPeerCount = len(peers)
return user, nil
}
func (m Manager) GetAllUsers(ctx context.Context) ([]domain.User, error) {
if err := domain.ValidateAdminAccessRights(ctx); err != nil {
return nil, err