diff --git a/docs/documentation/rest-api/swagger.yaml b/docs/documentation/rest-api/swagger.yaml index 46c00ae..fb6cd00 100644 --- a/docs/documentation/rest-api/swagger.yaml +++ b/docs/documentation/rest-api/swagger.yaml @@ -692,6 +692,7 @@ paths: tags: - Interfaces put: + description: This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...). operationId: interfaces_handleUpdatePut parameters: - description: The interface identifier. @@ -739,6 +740,7 @@ paths: - Interfaces /interface/new: post: + description: This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...). operationId: interfaces_handleCreatePost parameters: - description: The interface data. @@ -779,6 +781,34 @@ paths: summary: Create a new interface record. tags: - Interfaces + /interface/prepare: + get: + description: This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...). + operationId: interfaces_handlePrepareGet + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Interface' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Prepare a new interface record. + tags: + - Interfaces /metrics/by-interface/{id}: get: operationId: metrics_handleMetricsForInterfaceGet @@ -967,7 +997,7 @@ paths: tags: - Peers put: - description: Only admins can update existing records. + description: Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs). operationId: peers_handleUpdatePut parameters: - description: The peer identifier. @@ -1078,7 +1108,7 @@ paths: - Peers /peer/new: post: - description: Only admins can create new records. + description: Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs). operationId: peers_handleCreatePost parameters: - description: The peer data. @@ -1119,6 +1149,48 @@ paths: summary: Create a new peer record. tags: - Peers + /peer/prepare/{id}: + get: + description: This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address. + operationId: peers_handlePrepareGet + parameters: + - description: The interface identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Peer' + "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: Prepare a new peer record for the given WireGuard interface. + tags: + - Peers /provisioning/data/peer-config: get: description: Normal users can only access their own record. Admins can access all records. diff --git a/internal/app/api/core/assets/doc/v1_swagger.json b/internal/app/api/core/assets/doc/v1_swagger.json index 56f3ffb..c479a70 100644 --- a/internal/app/api/core/assets/doc/v1_swagger.json +++ b/internal/app/api/core/assets/doc/v1_swagger.json @@ -118,6 +118,7 @@ "BasicAuth": [] } ], + "description": "This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).", "produces": [ "application/json" ], @@ -250,6 +251,7 @@ "BasicAuth": [] } ], + "description": "This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...).", "produces": [ "application/json" ], @@ -309,6 +311,50 @@ } } }, + "/interface/prepare": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...).", + "produces": [ + "application/json" + ], + "tags": [ + "Interfaces" + ], + "summary": "Prepare a new interface record.", + "operationId": "interfaces_handlePrepareGet", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Interface" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "403": { + "description": "Forbidden", + "schema": { + "$ref": "#/definitions/models.Error" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/models.Error" + } + } + } + } + }, "/metrics/by-interface/{id}": { "get": { "security": [ @@ -547,7 +593,7 @@ "BasicAuth": [] } ], - "description": "Only admins can update existing records.", + "description": "Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs).", "produces": [ "application/json" ], @@ -779,7 +825,7 @@ "BasicAuth": [] } ], - "description": "Only admins can create new records.", + "description": "Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs).", "produces": [ "application/json" ], @@ -839,6 +885,71 @@ } } }, + "/peer/prepare/{id}": { + "get": { + "security": [ + { + "BasicAuth": [] + } + ], + "description": "This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address.", + "produces": [ + "application/json" + ], + "tags": [ + "Peers" + ], + "summary": "Prepare a new peer record for the given WireGuard interface.", + "operationId": "peers_handlePrepareGet", + "parameters": [ + { + "type": "string", + "description": "The interface identifier.", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Peer" + } + }, + "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-config": { "get": { "security": [ diff --git a/internal/app/api/core/assets/doc/v1_swagger.yaml b/internal/app/api/core/assets/doc/v1_swagger.yaml index 01b8d19..295bd79 100644 --- a/internal/app/api/core/assets/doc/v1_swagger.yaml +++ b/internal/app/api/core/assets/doc/v1_swagger.yaml @@ -748,6 +748,8 @@ paths: tags: - Interfaces put: + description: This endpoint updates an existing interface with the provided data. + All required fields must be filled (e.g. name, private key, public key, ...). operationId: interfaces_handleUpdatePut parameters: - description: The interface identifier. @@ -795,6 +797,8 @@ paths: - Interfaces /interface/new: post: + description: This endpoint creates a new interface with the provided data. All + required fields must be filled (e.g. name, private key, public key, ...). operationId: interfaces_handleCreatePost parameters: - description: The interface data. @@ -835,6 +839,35 @@ paths: summary: Create a new interface record. tags: - Interfaces + /interface/prepare: + get: + description: This endpoint returns a new interface with default values (fresh + key pair, valid name, new IP address pool, ...). + operationId: interfaces_handlePrepareGet + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Interface' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/models.Error' + "403": + description: Forbidden + schema: + $ref: '#/definitions/models.Error' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/models.Error' + security: + - BasicAuth: [] + summary: Prepare a new interface record. + tags: + - Interfaces /metrics/by-interface/{id}: get: operationId: metrics_handleMetricsForInterfaceGet @@ -1024,7 +1057,8 @@ paths: tags: - Peers put: - description: Only admins can update existing records. + description: Only admins can update existing records. The peer record must contain + all required fields (e.g., public key, allowed IPs). operationId: peers_handleUpdatePut parameters: - description: The peer identifier. @@ -1136,7 +1170,8 @@ paths: - Peers /peer/new: post: - description: Only admins can create new records. + description: Only admins can create new records. The peer record must contain + all required fields (e.g., public key, allowed IPs). operationId: peers_handleCreatePost parameters: - description: The peer data. @@ -1177,6 +1212,49 @@ paths: summary: Create a new peer record. tags: - Peers + /peer/prepare/{id}: + get: + description: This endpoint is used to prepare a new peer record. The returned + data contains a fresh key pair and valid ip address. + operationId: peers_handlePrepareGet + parameters: + - description: The interface identifier. + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Peer' + "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: Prepare a new peer record for the given WireGuard interface. + tags: + - Peers /provisioning/data/peer-config: get: description: Normal users can only access their own record. Admins can access diff --git a/internal/app/api/v1/backend/interface_service.go b/internal/app/api/v1/backend/interface_service.go index fcf6871..35f0b84 100644 --- a/internal/app/api/v1/backend/interface_service.go +++ b/internal/app/api/v1/backend/interface_service.go @@ -11,6 +11,7 @@ import ( type InterfaceServiceInterfaceManagerRepo interface { GetAllInterfacesAndPeers(ctx context.Context) ([]domain.Interface, [][]domain.Peer, error) GetInterfaceAndPeers(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error) + PrepareInterface(ctx context.Context) (*domain.Interface, 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 @@ -60,6 +61,19 @@ func (s InterfaceService) GetById(ctx context.Context, id domain.InterfaceIdenti return interfaceData, interfacePeers, nil } +func (s InterfaceService) Prepare(ctx context.Context) (*domain.Interface, error) { + if err := domain.ValidateAdminAccessRights(ctx); err != nil { + return nil, err + } + + interfaceData, err := s.interfaces.PrepareInterface(ctx) + if err != nil { + return nil, err + } + + return interfaceData, nil +} + func (s InterfaceService) Create(ctx context.Context, iface *domain.Interface) (*domain.Interface, error) { if err := domain.ValidateAdminAccessRights(ctx); err != nil { return nil, err diff --git a/internal/app/api/v1/backend/peer_service.go b/internal/app/api/v1/backend/peer_service.go index 5210c45..fbfe186 100644 --- a/internal/app/api/v1/backend/peer_service.go +++ b/internal/app/api/v1/backend/peer_service.go @@ -13,6 +13,7 @@ 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) + PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*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 @@ -95,6 +96,19 @@ func (s PeerService) GetById(ctx context.Context, id domain.PeerIdentifier) (*do return peer, nil } +func (s PeerService) Prepare(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error) { + if err := domain.ValidateAdminAccessRights(ctx); err != nil { + return nil, err + } + + peer, err := s.peers.PreparePeer(ctx, id) + if 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 diff --git a/internal/app/api/v1/handlers/endpoint_interface.go b/internal/app/api/v1/handlers/endpoint_interface.go index a0c9d5f..1ea7241 100644 --- a/internal/app/api/v1/handlers/endpoint_interface.go +++ b/internal/app/api/v1/handlers/endpoint_interface.go @@ -15,6 +15,7 @@ import ( type InterfaceEndpointInterfaceService interface { GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error) GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error) + Prepare(context.Context) (*domain.Interface, 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 @@ -49,6 +50,7 @@ func (e InterfaceEndpoint) RegisterRoutes(g *routegroup.Bundle) { apiGroup.HandleFunc("GET /all", e.handleAllGet()) apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet()) + apiGroup.HandleFunc("GET /prepare", e.handlePrepareGet()) apiGroup.HandleFunc("POST /new", e.handleCreatePost()) apiGroup.HandleFunc("PUT /by-id/{id}", e.handleUpdatePut()) apiGroup.HandleFunc("DELETE /by-id/{id}", e.handleDelete()) @@ -112,11 +114,38 @@ func (e InterfaceEndpoint) handleByIdGet() http.HandlerFunc { } } +// handlePrepareGet returns a gorm handler function. +// +// @ID interfaces_handlePrepareGet +// @Tags Interfaces +// @Summary Prepare a new interface record. +// @Description This endpoint returns a new interface with default values (fresh key pair, valid name, new IP address pool, ...). +// @Produce json +// @Success 200 {object} models.Interface +// @Failure 401 {object} models.Error +// @Failure 403 {object} models.Error +// @Failure 500 {object} models.Error +// @Router /interface/prepare [get] +// @Security BasicAuth +func (e InterfaceEndpoint) handlePrepareGet() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + iface, err := e.interfaces.Prepare(r.Context()) + if err != nil { + status, model := ParseServiceError(err) + respond.JSON(w, status, model) + return + } + + respond.JSON(w, http.StatusOK, models.NewInterface(iface, nil)) + } +} + // handleCreatePost returns a gorm handler function. // // @ID interfaces_handleCreatePost // @Tags Interfaces // @Summary Create a new interface record. +// @Description This endpoint creates a new interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...). // @Param request body models.Interface true "The interface data." // @Produce json // @Success 200 {object} models.Interface @@ -155,6 +184,7 @@ func (e InterfaceEndpoint) handleCreatePost() http.HandlerFunc { // @ID interfaces_handleUpdatePut // @Tags Interfaces // @Summary Update an interface record. +// @Description This endpoint updates an existing interface with the provided data. All required fields must be filled (e.g. name, private key, public key, ...). // @Param id path string true "The interface identifier." // @Param request body models.Interface true "The interface data." // @Produce json diff --git a/internal/app/api/v1/handlers/endpoint_peer.go b/internal/app/api/v1/handlers/endpoint_peer.go index ad79d13..393143e 100644 --- a/internal/app/api/v1/handlers/endpoint_peer.go +++ b/internal/app/api/v1/handlers/endpoint_peer.go @@ -16,6 +16,7 @@ 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) + Prepare(ctx context.Context, id domain.InterfaceIdentifier) (*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 @@ -51,6 +52,7 @@ func (e PeerEndpoint) RegisterRoutes(g *routegroup.Bundle) { apiGroup.HandleFunc("GET /by-user/{id}", e.handleAllForUserGet()) apiGroup.HandleFunc("GET /by-id/{id}", e.handleByIdGet()) + apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("GET /prepare/{id}", e.handlePrepareGet()) apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("POST /new", e.handleCreatePost()) apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("PUT /by-id/{id}", e.handleUpdatePut()) apiGroup.With(e.authenticator.LoggedIn(ScopeAdmin)).HandleFunc("DELETE /by-id/{id}", e.handleDelete()) @@ -156,12 +158,48 @@ func (e PeerEndpoint) handleByIdGet() http.HandlerFunc { } } +// handlePrepareGet returns a gorm handler function. +// +// @ID peers_handlePrepareGet +// @Tags Peers +// @Summary Prepare a new peer record for the given WireGuard interface. +// @Description This endpoint is used to prepare a new peer record. The returned data contains a fresh key pair and valid ip address. +// @Param id path string true "The interface identifier." +// @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/prepare/{id} [get] +// @Security BasicAuth +func (e PeerEndpoint) handlePrepareGet() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := request.Path(r, "id") + if id == "" { + respond.JSON(w, http.StatusBadRequest, + models.Error{Code: http.StatusBadRequest, Message: "missing interface id"}) + return + } + + peer, err := e.peers.Prepare(r.Context(), domain.InterfaceIdentifier(id)) + if err != nil { + status, model := ParseServiceError(err) + respond.JSON(w, status, model) + return + } + + respond.JSON(w, 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. +// @Description Only admins can create new records. The peer record must contain all required fields (e.g., public key, allowed IPs). // @Param request body models.Peer true "The peer data." // @Produce json // @Success 200 {object} models.Peer @@ -200,7 +238,7 @@ func (e PeerEndpoint) handleCreatePost() http.HandlerFunc { // @ID peers_handleUpdatePut // @Tags Peers // @Summary Update a peer record. -// @Description Only admins can update existing records. +// @Description Only admins can update existing records. The peer record must contain all required fields (e.g., public key, allowed IPs). // @Param id path string true "The peer identifier." // @Param request body models.Peer true "The peer data." // @Produce json