mirror of
				https://github.com/h44z/wg-portal.git
				synced 2025-11-03 23:56:18 +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:
		@@ -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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
    "swagger": "2.0",
 | 
			
		||||
    "info": {
 | 
			
		||||
        "description": "WireGuard Portal API - a testing API endpoint",
 | 
			
		||||
        "title": "WireGuard Portal API",
 | 
			
		||||
        "description": "WireGuard Portal API - UI Endpoints",
 | 
			
		||||
        "title": "WireGuard Portal SPA-UI API",
 | 
			
		||||
        "contact": {
 | 
			
		||||
            "name": "WireGuard Portal Developers",
 | 
			
		||||
            "url": "https://github.com/h44z/wg-portal"
 | 
			
		||||
@@ -175,6 +175,26 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/config/settings": {
 | 
			
		||||
            "get": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
                    "Configuration"
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Get the frontend settings object.",
 | 
			
		||||
                "operationId": "config_handleSettingsGet",
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "200": {
 | 
			
		||||
                        "description": "The JavaScript contents",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "type": "string"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/csrf": {
 | 
			
		||||
            "get": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
@@ -499,6 +519,91 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/interface/{id}/apply-peer-defaults": {
 | 
			
		||||
            "post": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
                    "Interface"
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Apply all peer defaults to the available peers.",
 | 
			
		||||
                "operationId": "interfaces_handleApplyPeerDefaultsPost",
 | 
			
		||||
                "parameters": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "string",
 | 
			
		||||
                        "description": "The interface identifier",
 | 
			
		||||
                        "name": "id",
 | 
			
		||||
                        "in": "path",
 | 
			
		||||
                        "required": true
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "description": "The interface data",
 | 
			
		||||
                        "name": "request",
 | 
			
		||||
                        "in": "body",
 | 
			
		||||
                        "required": true,
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Interface"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "204": {
 | 
			
		||||
                        "description": "No content if applying peer defaults was successful"
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
                        "description": "Bad Request",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "500": {
 | 
			
		||||
                        "description": "Internal Server Error",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/interface/{id}/save-config": {
 | 
			
		||||
            "post": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
                    "Interface"
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Save the interface configuration in wg-quick format to a file.",
 | 
			
		||||
                "operationId": "interfaces_handleSaveConfigPost",
 | 
			
		||||
                "parameters": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "string",
 | 
			
		||||
                        "description": "The interface identifier",
 | 
			
		||||
                        "name": "id",
 | 
			
		||||
                        "in": "path",
 | 
			
		||||
                        "required": true
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "204": {
 | 
			
		||||
                        "description": "No content if saving the configuration was successful"
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
                        "description": "Bad Request",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "500": {
 | 
			
		||||
                        "description": "Internal Server Error",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/now": {
 | 
			
		||||
            "get": {
 | 
			
		||||
                "description": "Nothing more to describe...",
 | 
			
		||||
@@ -526,9 +631,50 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/peer/config-mail": {
 | 
			
		||||
            "post": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
                    "Peer"
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Send peer configuration via email.",
 | 
			
		||||
                "operationId": "peers_handleEmailPost",
 | 
			
		||||
                "parameters": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "description": "The peer mail request data",
 | 
			
		||||
                        "name": "request",
 | 
			
		||||
                        "in": "body",
 | 
			
		||||
                        "required": true,
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.PeerMailRequest"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "204": {
 | 
			
		||||
                        "description": "No content if mail sending was successful"
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
                        "description": "Bad Request",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "500": {
 | 
			
		||||
                        "description": "Internal Server Error",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/peer/config-qr/{id}": {
 | 
			
		||||
            "get": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "image/png",
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
@@ -536,11 +682,20 @@
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Get peer configuration as qr code.",
 | 
			
		||||
                "operationId": "peers_handleQrCodeGet",
 | 
			
		||||
                "parameters": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "string",
 | 
			
		||||
                        "description": "The peer identifier",
 | 
			
		||||
                        "name": "id",
 | 
			
		||||
                        "in": "path",
 | 
			
		||||
                        "required": true
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "200": {
 | 
			
		||||
                        "description": "OK",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "type": "string"
 | 
			
		||||
                            "type": "file"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
@@ -568,6 +723,15 @@
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Get peer configuration as string.",
 | 
			
		||||
                "operationId": "peers_handleConfigGet",
 | 
			
		||||
                "parameters": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "string",
 | 
			
		||||
                        "description": "The peer identifier",
 | 
			
		||||
                        "name": "id",
 | 
			
		||||
                        "in": "path",
 | 
			
		||||
                        "required": true
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "200": {
 | 
			
		||||
                        "description": "OK",
 | 
			
		||||
@@ -634,6 +798,59 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/peer/iface/{iface}/multiplenew": {
 | 
			
		||||
            "post": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
                    "Peer"
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Create multiple new peers for the given interface.",
 | 
			
		||||
                "operationId": "peers_handleCreateMultiplePost",
 | 
			
		||||
                "parameters": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "string",
 | 
			
		||||
                        "description": "The interface identifier",
 | 
			
		||||
                        "name": "iface",
 | 
			
		||||
                        "in": "path",
 | 
			
		||||
                        "required": true
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "description": "The peer creation request data",
 | 
			
		||||
                        "name": "request",
 | 
			
		||||
                        "in": "body",
 | 
			
		||||
                        "required": true,
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.MultiPeerRequest"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "200": {
 | 
			
		||||
                        "description": "OK",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "type": "array",
 | 
			
		||||
                            "items": {
 | 
			
		||||
                                "$ref": "#/definitions/model.Peer"
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
                        "description": "Bad Request",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "500": {
 | 
			
		||||
                        "description": "Internal Server Error",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/peer/iface/{iface}/new": {
 | 
			
		||||
            "post": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
@@ -725,6 +942,47 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/peer/iface/{iface}/stats": {
 | 
			
		||||
            "get": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
                    "Peer"
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Get peer stats for the given interface.",
 | 
			
		||||
                "operationId": "peers_handleStatsGet",
 | 
			
		||||
                "parameters": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": "string",
 | 
			
		||||
                        "description": "The interface identifier",
 | 
			
		||||
                        "name": "iface",
 | 
			
		||||
                        "in": "path",
 | 
			
		||||
                        "required": true
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "200": {
 | 
			
		||||
                        "description": "OK",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.PeerStats"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
                        "description": "Bad Request",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "500": {
 | 
			
		||||
                        "description": "Internal Server Error",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/peer/{id}": {
 | 
			
		||||
            "get": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
@@ -1041,6 +1299,70 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/user/{id}/api/disable": {
 | 
			
		||||
            "post": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
                    "Users"
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Disable the REST API for the given user.",
 | 
			
		||||
                "operationId": "users_handleApiDisablePost",
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "200": {
 | 
			
		||||
                        "description": "OK",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.User"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
                        "description": "Bad Request",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "500": {
 | 
			
		||||
                        "description": "Internal Server Error",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/user/{id}/api/enable": {
 | 
			
		||||
            "post": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
                    "Users"
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Enable the REST API for the given user.",
 | 
			
		||||
                "operationId": "users_handleApiEnablePost",
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "200": {
 | 
			
		||||
                        "description": "OK",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.User"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
                        "description": "Bad Request",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "500": {
 | 
			
		||||
                        "description": "Internal Server Error",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/user/{id}/peers": {
 | 
			
		||||
            "get": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
@@ -1061,6 +1383,44 @@
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
                        "description": "Bad Request",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "500": {
 | 
			
		||||
                        "description": "Internal Server Error",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "/user/{id}/stats": {
 | 
			
		||||
            "get": {
 | 
			
		||||
                "produces": [
 | 
			
		||||
                    "application/json"
 | 
			
		||||
                ],
 | 
			
		||||
                "tags": [
 | 
			
		||||
                    "Users"
 | 
			
		||||
                ],
 | 
			
		||||
                "summary": "Get peer stats for the given user.",
 | 
			
		||||
                "operationId": "users_handleStatsGet",
 | 
			
		||||
                "responses": {
 | 
			
		||||
                    "200": {
 | 
			
		||||
                        "description": "OK",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.PeerStats"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "400": {
 | 
			
		||||
                        "description": "Bad Request",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
                            "$ref": "#/definitions/model.Error"
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                    "500": {
 | 
			
		||||
                        "description": "Internal Server Error",
 | 
			
		||||
                        "schema": {
 | 
			
		||||
@@ -1072,6 +1432,53 @@
 | 
			
		||||
        }
 | 
			
		||||
    },
 | 
			
		||||
    "definitions": {
 | 
			
		||||
        "model.ConfigOption-array_string": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Overridable": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "Value": {
 | 
			
		||||
                    "type": "array",
 | 
			
		||||
                    "items": {
 | 
			
		||||
                        "type": "string"
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.ConfigOption-int": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Overridable": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "Value": {
 | 
			
		||||
                    "type": "integer"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.ConfigOption-string": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Overridable": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "Value": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.ConfigOption-uint32": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Overridable": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "Value": {
 | 
			
		||||
                    "type": "integer"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.Error": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
@@ -1083,25 +1490,11 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.Int32ConfigOption": {
 | 
			
		||||
        "model.ExpiryDate": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Overridable": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "Value": {
 | 
			
		||||
                    "type": "integer"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.IntConfigOption": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Overridable": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "Value": {
 | 
			
		||||
                    "type": "integer"
 | 
			
		||||
                "time.Time": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
@@ -1290,6 +1683,20 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.MultiPeerRequest": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Identifiers": {
 | 
			
		||||
                    "type": "array",
 | 
			
		||||
                    "items": {
 | 
			
		||||
                        "type": "string"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "Suffix": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.Peer": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
@@ -1304,7 +1711,7 @@
 | 
			
		||||
                    "description": "all allowed ip subnets, comma seperated",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringSliceConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-array_string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1328,7 +1735,7 @@
 | 
			
		||||
                    "description": "the dns server that should be set if the interface is up, comma separated",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringSliceConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-array_string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1336,7 +1743,7 @@
 | 
			
		||||
                    "description": "the dns search option string that should be set if the interface is up, will be appended to DnsStr",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringSliceConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-array_string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1344,7 +1751,7 @@
 | 
			
		||||
                    "description": "the endpoint address",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1352,13 +1759,17 @@
 | 
			
		||||
                    "description": "the endpoint public key",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
                "ExpiresAt": {
 | 
			
		||||
                    "description": "expiry dates for peers",
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.ExpiryDate"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
                "ExtraAllowedIPs": {
 | 
			
		||||
                    "description": "all allowed ip subnets on the server side, comma seperated",
 | 
			
		||||
@@ -1371,7 +1782,7 @@
 | 
			
		||||
                    "description": "a firewall mark",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.Int32ConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-uint32"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1392,7 +1803,7 @@
 | 
			
		||||
                    "description": "the device MTU",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.IntConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-int"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1404,7 +1815,7 @@
 | 
			
		||||
                    "description": "the persistent keep-alive interval",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.IntConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-int"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1412,7 +1823,7 @@
 | 
			
		||||
                    "description": "action that is executed after the device is down",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1420,7 +1831,7 @@
 | 
			
		||||
                    "description": "action that is executed after the device is up",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1428,7 +1839,7 @@
 | 
			
		||||
                    "description": "action that is executed before the device is down",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1436,7 +1847,7 @@
 | 
			
		||||
                    "description": "action that is executed before the device is up",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1458,7 +1869,7 @@
 | 
			
		||||
                    "description": "the routing table",
 | 
			
		||||
                    "allOf": [
 | 
			
		||||
                        {
 | 
			
		||||
                            "$ref": "#/definitions/model.StringConfigOption"
 | 
			
		||||
                            "$ref": "#/definitions/model.ConfigOption-string"
 | 
			
		||||
                        }
 | 
			
		||||
                    ]
 | 
			
		||||
                },
 | 
			
		||||
@@ -1468,6 +1879,66 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.PeerMailRequest": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Identifiers": {
 | 
			
		||||
                    "type": "array",
 | 
			
		||||
                    "items": {
 | 
			
		||||
                        "type": "string"
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                "LinkOnly": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.PeerStatData": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "BytesReceived": {
 | 
			
		||||
                    "type": "integer"
 | 
			
		||||
                },
 | 
			
		||||
                "BytesTransmitted": {
 | 
			
		||||
                    "type": "integer"
 | 
			
		||||
                },
 | 
			
		||||
                "EndpointAddress": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "IsConnected": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "IsPingable": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "LastHandshake": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "LastPing": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "LastSessionStart": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.PeerStats": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Enabled": {
 | 
			
		||||
                    "description": "peer stats tracking enabled",
 | 
			
		||||
                    "type": "boolean",
 | 
			
		||||
                    "example": true
 | 
			
		||||
                },
 | 
			
		||||
                "Stats": {
 | 
			
		||||
                    "description": "stats, map key = Peer identifier",
 | 
			
		||||
                    "type": "object",
 | 
			
		||||
                    "additionalProperties": {
 | 
			
		||||
                        "$ref": "#/definitions/model.PeerStatData"
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.SessionInfo": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
@@ -1491,34 +1962,35 @@
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.StringConfigOption": {
 | 
			
		||||
        "model.Settings": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Overridable": {
 | 
			
		||||
                "ApiAdminOnly": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "Value": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.StringSliceConfigOption": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "Overridable": {
 | 
			
		||||
                "MailLinkOnly": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "Value": {
 | 
			
		||||
                    "type": "array",
 | 
			
		||||
                    "items": {
 | 
			
		||||
                        "type": "string"
 | 
			
		||||
                    }
 | 
			
		||||
                "PersistentConfigSupported": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "SelfProvisioning": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        "model.User": {
 | 
			
		||||
            "type": "object",
 | 
			
		||||
            "properties": {
 | 
			
		||||
                "ApiEnabled": {
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "ApiToken": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "ApiTokenCreated": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "Department": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
@@ -1545,6 +2017,14 @@
 | 
			
		||||
                "Lastname": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "Locked": {
 | 
			
		||||
                    "description": "if this field is set, the user is locked",
 | 
			
		||||
                    "type": "boolean"
 | 
			
		||||
                },
 | 
			
		||||
                "LockedReason": {
 | 
			
		||||
                    "description": "the reason why the user has been locked",
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
                "Notes": {
 | 
			
		||||
                    "type": "string"
 | 
			
		||||
                },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,35 @@
 | 
			
		||||
basePath: /api/v0
 | 
			
		||||
definitions:
 | 
			
		||||
  model.ConfigOption-array_string:
 | 
			
		||||
    properties:
 | 
			
		||||
      Overridable:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Value:
 | 
			
		||||
        items:
 | 
			
		||||
          type: string
 | 
			
		||||
        type: array
 | 
			
		||||
    type: object
 | 
			
		||||
  model.ConfigOption-int:
 | 
			
		||||
    properties:
 | 
			
		||||
      Overridable:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Value:
 | 
			
		||||
        type: integer
 | 
			
		||||
    type: object
 | 
			
		||||
  model.ConfigOption-string:
 | 
			
		||||
    properties:
 | 
			
		||||
      Overridable:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Value:
 | 
			
		||||
        type: string
 | 
			
		||||
    type: object
 | 
			
		||||
  model.ConfigOption-uint32:
 | 
			
		||||
    properties:
 | 
			
		||||
      Overridable:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Value:
 | 
			
		||||
        type: integer
 | 
			
		||||
    type: object
 | 
			
		||||
  model.Error:
 | 
			
		||||
    properties:
 | 
			
		||||
      Code:
 | 
			
		||||
@@ -7,19 +37,10 @@ definitions:
 | 
			
		||||
      Message:
 | 
			
		||||
        type: string
 | 
			
		||||
    type: object
 | 
			
		||||
  model.Int32ConfigOption:
 | 
			
		||||
  model.ExpiryDate:
 | 
			
		||||
    properties:
 | 
			
		||||
      Overridable:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Value:
 | 
			
		||||
        type: integer
 | 
			
		||||
    type: object
 | 
			
		||||
  model.IntConfigOption:
 | 
			
		||||
    properties:
 | 
			
		||||
      Overridable:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Value:
 | 
			
		||||
        type: integer
 | 
			
		||||
      time.Time:
 | 
			
		||||
        type: string
 | 
			
		||||
    type: object
 | 
			
		||||
  model.Interface:
 | 
			
		||||
    properties:
 | 
			
		||||
@@ -160,6 +181,15 @@ definitions:
 | 
			
		||||
        example: /auth/google/login
 | 
			
		||||
        type: string
 | 
			
		||||
    type: object
 | 
			
		||||
  model.MultiPeerRequest:
 | 
			
		||||
    properties:
 | 
			
		||||
      Identifiers:
 | 
			
		||||
        items:
 | 
			
		||||
          type: string
 | 
			
		||||
        type: array
 | 
			
		||||
      Suffix:
 | 
			
		||||
        type: string
 | 
			
		||||
    type: object
 | 
			
		||||
  model.Peer:
 | 
			
		||||
    properties:
 | 
			
		||||
      Addresses:
 | 
			
		||||
@@ -169,7 +199,7 @@ definitions:
 | 
			
		||||
        type: array
 | 
			
		||||
      AllowedIPs:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringSliceConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-array_string'
 | 
			
		||||
        description: all allowed ip subnets, comma seperated
 | 
			
		||||
      CheckAliveAddress:
 | 
			
		||||
        description: optional ip address or DNS name that is used for ping checks
 | 
			
		||||
@@ -185,25 +215,26 @@ definitions:
 | 
			
		||||
        type: string
 | 
			
		||||
      Dns:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringSliceConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-array_string'
 | 
			
		||||
        description: the dns server that should be set if the interface is up, comma
 | 
			
		||||
          separated
 | 
			
		||||
      DnsSearch:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringSliceConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-array_string'
 | 
			
		||||
        description: the dns search option string that should be set if the interface
 | 
			
		||||
          is up, will be appended to DnsStr
 | 
			
		||||
      Endpoint:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-string'
 | 
			
		||||
        description: the endpoint address
 | 
			
		||||
      EndpointPublicKey:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-string'
 | 
			
		||||
        description: the endpoint public key
 | 
			
		||||
      ExpiresAt:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.ExpiryDate'
 | 
			
		||||
        description: expiry dates for peers
 | 
			
		||||
        type: string
 | 
			
		||||
      ExtraAllowedIPs:
 | 
			
		||||
        description: all allowed ip subnets on the server side, comma seperated
 | 
			
		||||
        items:
 | 
			
		||||
@@ -211,7 +242,7 @@ definitions:
 | 
			
		||||
        type: array
 | 
			
		||||
      FirewallMark:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.Int32ConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-uint32'
 | 
			
		||||
        description: a firewall mark
 | 
			
		||||
      Identifier:
 | 
			
		||||
        description: peer unique identifier
 | 
			
		||||
@@ -225,30 +256,30 @@ definitions:
 | 
			
		||||
        type: string
 | 
			
		||||
      Mtu:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.IntConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-int'
 | 
			
		||||
        description: the device MTU
 | 
			
		||||
      Notes:
 | 
			
		||||
        description: a note field for peers
 | 
			
		||||
        type: string
 | 
			
		||||
      PersistentKeepalive:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.IntConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-int'
 | 
			
		||||
        description: the persistent keep-alive interval
 | 
			
		||||
      PostDown:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-string'
 | 
			
		||||
        description: action that is executed after the device is down
 | 
			
		||||
      PostUp:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-string'
 | 
			
		||||
        description: action that is executed after the device is up
 | 
			
		||||
      PreDown:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-string'
 | 
			
		||||
        description: action that is executed before the device is down
 | 
			
		||||
      PreUp:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-string'
 | 
			
		||||
        description: action that is executed before the device is up
 | 
			
		||||
      PresharedKey:
 | 
			
		||||
        description: the pre-shared Key of the peer
 | 
			
		||||
@@ -263,12 +294,52 @@ definitions:
 | 
			
		||||
        type: string
 | 
			
		||||
      RoutingTable:
 | 
			
		||||
        allOf:
 | 
			
		||||
        - $ref: '#/definitions/model.StringConfigOption'
 | 
			
		||||
        - $ref: '#/definitions/model.ConfigOption-string'
 | 
			
		||||
        description: the routing table
 | 
			
		||||
      UserIdentifier:
 | 
			
		||||
        description: the owner
 | 
			
		||||
        type: string
 | 
			
		||||
    type: object
 | 
			
		||||
  model.PeerMailRequest:
 | 
			
		||||
    properties:
 | 
			
		||||
      Identifiers:
 | 
			
		||||
        items:
 | 
			
		||||
          type: string
 | 
			
		||||
        type: array
 | 
			
		||||
      LinkOnly:
 | 
			
		||||
        type: boolean
 | 
			
		||||
    type: object
 | 
			
		||||
  model.PeerStatData:
 | 
			
		||||
    properties:
 | 
			
		||||
      BytesReceived:
 | 
			
		||||
        type: integer
 | 
			
		||||
      BytesTransmitted:
 | 
			
		||||
        type: integer
 | 
			
		||||
      EndpointAddress:
 | 
			
		||||
        type: string
 | 
			
		||||
      IsConnected:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      IsPingable:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      LastHandshake:
 | 
			
		||||
        type: string
 | 
			
		||||
      LastPing:
 | 
			
		||||
        type: string
 | 
			
		||||
      LastSessionStart:
 | 
			
		||||
        type: string
 | 
			
		||||
    type: object
 | 
			
		||||
  model.PeerStats:
 | 
			
		||||
    properties:
 | 
			
		||||
      Enabled:
 | 
			
		||||
        description: peer stats tracking enabled
 | 
			
		||||
        example: true
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Stats:
 | 
			
		||||
        additionalProperties:
 | 
			
		||||
          $ref: '#/definitions/model.PeerStatData'
 | 
			
		||||
        description: stats, map key = Peer identifier
 | 
			
		||||
        type: object
 | 
			
		||||
    type: object
 | 
			
		||||
  model.SessionInfo:
 | 
			
		||||
    properties:
 | 
			
		||||
      IsAdmin:
 | 
			
		||||
@@ -284,24 +355,25 @@ definitions:
 | 
			
		||||
      UserLastname:
 | 
			
		||||
        type: string
 | 
			
		||||
    type: object
 | 
			
		||||
  model.StringConfigOption:
 | 
			
		||||
  model.Settings:
 | 
			
		||||
    properties:
 | 
			
		||||
      Overridable:
 | 
			
		||||
      ApiAdminOnly:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Value:
 | 
			
		||||
        type: string
 | 
			
		||||
    type: object
 | 
			
		||||
  model.StringSliceConfigOption:
 | 
			
		||||
    properties:
 | 
			
		||||
      Overridable:
 | 
			
		||||
      MailLinkOnly:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      PersistentConfigSupported:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      SelfProvisioning:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Value:
 | 
			
		||||
        items:
 | 
			
		||||
          type: string
 | 
			
		||||
        type: array
 | 
			
		||||
    type: object
 | 
			
		||||
  model.User:
 | 
			
		||||
    properties:
 | 
			
		||||
      ApiEnabled:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      ApiToken:
 | 
			
		||||
        type: string
 | 
			
		||||
      ApiTokenCreated:
 | 
			
		||||
        type: string
 | 
			
		||||
      Department:
 | 
			
		||||
        type: string
 | 
			
		||||
      Disabled:
 | 
			
		||||
@@ -320,6 +392,12 @@ definitions:
 | 
			
		||||
        type: boolean
 | 
			
		||||
      Lastname:
 | 
			
		||||
        type: string
 | 
			
		||||
      Locked:
 | 
			
		||||
        description: if this field is set, the user is locked
 | 
			
		||||
        type: boolean
 | 
			
		||||
      LockedReason:
 | 
			
		||||
        description: the reason why the user has been locked
 | 
			
		||||
        type: string
 | 
			
		||||
      Notes:
 | 
			
		||||
        type: string
 | 
			
		||||
      Password:
 | 
			
		||||
@@ -337,8 +415,8 @@ info:
 | 
			
		||||
  contact:
 | 
			
		||||
    name: WireGuard Portal Developers
 | 
			
		||||
    url: https://github.com/h44z/wg-portal
 | 
			
		||||
  description: WireGuard Portal API - a testing API endpoint
 | 
			
		||||
  title: WireGuard Portal API
 | 
			
		||||
  description: WireGuard Portal API - UI Endpoints
 | 
			
		||||
  title: WireGuard Portal SPA-UI API
 | 
			
		||||
  version: "0.0"
 | 
			
		||||
paths:
 | 
			
		||||
  /auth/{provider}/callback:
 | 
			
		||||
@@ -448,6 +526,19 @@ paths:
 | 
			
		||||
      summary: Get the dynamic frontend configuration javascript.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Configuration
 | 
			
		||||
  /config/settings:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: config_handleSettingsGet
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: The JavaScript contents
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
      summary: Get the frontend settings object.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Configuration
 | 
			
		||||
  /csrf:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: base_handleCsrfGet
 | 
			
		||||
@@ -536,6 +627,62 @@ paths:
 | 
			
		||||
      summary: Update the interface record.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Interface
 | 
			
		||||
  /interface/{id}/apply-peer-defaults:
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: interfaces_handleApplyPeerDefaultsPost
 | 
			
		||||
      parameters:
 | 
			
		||||
      - description: The interface identifier
 | 
			
		||||
        in: path
 | 
			
		||||
        name: id
 | 
			
		||||
        required: true
 | 
			
		||||
        type: string
 | 
			
		||||
      - description: The interface data
 | 
			
		||||
        in: body
 | 
			
		||||
        name: request
 | 
			
		||||
        required: true
 | 
			
		||||
        schema:
 | 
			
		||||
          $ref: '#/definitions/model.Interface'
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "204":
 | 
			
		||||
          description: No content if applying peer defaults was successful
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
        "500":
 | 
			
		||||
          description: Internal Server Error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
      summary: Apply all peer defaults to the available peers.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Interface
 | 
			
		||||
  /interface/{id}/save-config:
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: interfaces_handleSaveConfigPost
 | 
			
		||||
      parameters:
 | 
			
		||||
      - description: The interface identifier
 | 
			
		||||
        in: path
 | 
			
		||||
        name: id
 | 
			
		||||
        required: true
 | 
			
		||||
        type: string
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "204":
 | 
			
		||||
          description: No content if saving the configuration was successful
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
        "500":
 | 
			
		||||
          description: Internal Server Error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
      summary: Save the interface configuration in wg-quick format to a file.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Interface
 | 
			
		||||
  /interface/all:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: interfaces_handleAllGet
 | 
			
		||||
@@ -762,16 +909,49 @@ paths:
 | 
			
		||||
      summary: Update the given peer record.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Peer
 | 
			
		||||
  /peer/config-mail:
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: peers_handleEmailPost
 | 
			
		||||
      parameters:
 | 
			
		||||
      - description: The peer mail request data
 | 
			
		||||
        in: body
 | 
			
		||||
        name: request
 | 
			
		||||
        required: true
 | 
			
		||||
        schema:
 | 
			
		||||
          $ref: '#/definitions/model.PeerMailRequest'
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "204":
 | 
			
		||||
          description: No content if mail sending was successful
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
        "500":
 | 
			
		||||
          description: Internal Server Error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
      summary: Send peer configuration via email.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Peer
 | 
			
		||||
  /peer/config-qr/{id}:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: peers_handleQrCodeGet
 | 
			
		||||
      parameters:
 | 
			
		||||
      - description: The peer identifier
 | 
			
		||||
        in: path
 | 
			
		||||
        name: id
 | 
			
		||||
        required: true
 | 
			
		||||
        type: string
 | 
			
		||||
      produces:
 | 
			
		||||
      - image/png
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: OK
 | 
			
		||||
          schema:
 | 
			
		||||
            type: string
 | 
			
		||||
            type: file
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
@@ -786,6 +966,12 @@ paths:
 | 
			
		||||
  /peer/config/{id}:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: peers_handleConfigGet
 | 
			
		||||
      parameters:
 | 
			
		||||
      - description: The peer identifier
 | 
			
		||||
        in: path
 | 
			
		||||
        name: id
 | 
			
		||||
        required: true
 | 
			
		||||
        type: string
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
@@ -833,6 +1019,41 @@ paths:
 | 
			
		||||
      summary: Get peers for the given interface.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Peer
 | 
			
		||||
  /peer/iface/{iface}/multiplenew:
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: peers_handleCreateMultiplePost
 | 
			
		||||
      parameters:
 | 
			
		||||
      - description: The interface identifier
 | 
			
		||||
        in: path
 | 
			
		||||
        name: iface
 | 
			
		||||
        required: true
 | 
			
		||||
        type: string
 | 
			
		||||
      - description: The peer creation request data
 | 
			
		||||
        in: body
 | 
			
		||||
        name: request
 | 
			
		||||
        required: true
 | 
			
		||||
        schema:
 | 
			
		||||
          $ref: '#/definitions/model.MultiPeerRequest'
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: OK
 | 
			
		||||
          schema:
 | 
			
		||||
            items:
 | 
			
		||||
              $ref: '#/definitions/model.Peer'
 | 
			
		||||
            type: array
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
        "500":
 | 
			
		||||
          description: Internal Server Error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
      summary: Create multiple new peers for the given interface.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Peer
 | 
			
		||||
  /peer/iface/{iface}/new:
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: peers_handleCreatePost
 | 
			
		||||
@@ -893,6 +1114,33 @@ paths:
 | 
			
		||||
      summary: Prepare a new peer for the given interface.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Peer
 | 
			
		||||
  /peer/iface/{iface}/stats:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: peers_handleStatsGet
 | 
			
		||||
      parameters:
 | 
			
		||||
      - description: The interface identifier
 | 
			
		||||
        in: path
 | 
			
		||||
        name: iface
 | 
			
		||||
        required: true
 | 
			
		||||
        type: string
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: OK
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.PeerStats'
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
        "500":
 | 
			
		||||
          description: Internal Server Error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
      summary: Get peer stats for the given interface.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Peer
 | 
			
		||||
  /user/{id}:
 | 
			
		||||
    delete:
 | 
			
		||||
      operationId: users_handleDelete
 | 
			
		||||
@@ -972,6 +1220,48 @@ paths:
 | 
			
		||||
      summary: Update the user record.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Users
 | 
			
		||||
  /user/{id}/api/disable:
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: users_handleApiDisablePost
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: OK
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.User'
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
        "500":
 | 
			
		||||
          description: Internal Server Error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
      summary: Disable the REST API for the given user.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Users
 | 
			
		||||
  /user/{id}/api/enable:
 | 
			
		||||
    post:
 | 
			
		||||
      operationId: users_handleApiEnablePost
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: OK
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.User'
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
        "500":
 | 
			
		||||
          description: Internal Server Error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
      summary: Enable the REST API for the given user.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Users
 | 
			
		||||
  /user/{id}/peers:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: users_handlePeersGet
 | 
			
		||||
@@ -984,6 +1274,10 @@ paths:
 | 
			
		||||
            items:
 | 
			
		||||
              $ref: '#/definitions/model.Peer'
 | 
			
		||||
            type: array
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
        "500":
 | 
			
		||||
          description: Internal Server Error
 | 
			
		||||
          schema:
 | 
			
		||||
@@ -991,6 +1285,27 @@ paths:
 | 
			
		||||
      summary: Get peers for the given user.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Users
 | 
			
		||||
  /user/{id}/stats:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: users_handleStatsGet
 | 
			
		||||
      produces:
 | 
			
		||||
      - application/json
 | 
			
		||||
      responses:
 | 
			
		||||
        "200":
 | 
			
		||||
          description: OK
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.PeerStats'
 | 
			
		||||
        "400":
 | 
			
		||||
          description: Bad Request
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
        "500":
 | 
			
		||||
          description: Internal Server Error
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: '#/definitions/model.Error'
 | 
			
		||||
      summary: Get peer stats for the given user.
 | 
			
		||||
      tags:
 | 
			
		||||
      - Users
 | 
			
		||||
  /user/all:
 | 
			
		||||
    get:
 | 
			
		||||
      operationId: users_handleAllGet
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1932
									
								
								internal/app/api/core/assets/doc/v1_swagger.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1932
									
								
								internal/app/api/core/assets/doc/v1_swagger.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1358
									
								
								internal/app/api/core/assets/doc/v1_swagger.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1358
									
								
								internal/app/api/core/assets/doc/v1_swagger.yaml
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										3917
									
								
								internal/app/api/core/assets/js/rapidoc-min.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3917
									
								
								internal/app/api/core/assets/js/rapidoc-min.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -8,10 +8,16 @@
 | 
			
		||||
    <rapi-doc
 | 
			
		||||
            spec-url="{{ $.ApiSpecUrl }}"
 | 
			
		||||
            theme="dark"
 | 
			
		||||
            render-style="focused"
 | 
			
		||||
            allow-server-selection="false"
 | 
			
		||||
            allow-authentication="false"
 | 
			
		||||
            allow-authentication="true"
 | 
			
		||||
            load-fonts="false"
 | 
			
		||||
            schema-style="table"
 | 
			
		||||
            schema-expand-level="1"
 | 
			
		||||
            default-schema-tab="model"
 | 
			
		||||
            fill-request-fields-with-example="true"
 | 
			
		||||
            show-method-in-nav-bar="as-colored-block"
 | 
			
		||||
            show-components="true"
 | 
			
		||||
            allow-spec-url-load="false"
 | 
			
		||||
            allow-spec-file-load="false"
 | 
			
		||||
            allow-spec-file-download="true"
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,8 @@ func NewServer(cfg *config.Config, endpoints ...ApiEndpointSetupFunc) (*Server,
 | 
			
		||||
	s.server.StaticFS("/doc", http.FS(fsMust(fs.Sub(apiStatics, "assets/doc"))))
 | 
			
		||||
 | 
			
		||||
	// Setup routes
 | 
			
		||||
	s.server.UseRawPath = true
 | 
			
		||||
	s.server.UnescapePathValues = true
 | 
			
		||||
	s.setupRoutes(endpoints...)
 | 
			
		||||
	s.setupFrontendRoutes()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,9 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/cors"
 | 
			
		||||
	"github.com/gin-contrib/sessions"
 | 
			
		||||
	"github.com/gin-contrib/sessions/memstore"
 | 
			
		||||
@@ -10,8 +13,6 @@ import (
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/v0/model"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/config"
 | 
			
		||||
	csrf "github.com/utrack/gin-csrf"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type handler interface {
 | 
			
		||||
@@ -20,12 +21,12 @@ type handler interface {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// To compile the API documentation use the
 | 
			
		||||
// build_tool
 | 
			
		||||
// command that can be found in the $PROJECT_ROOT/internal/ports/api/build_tool directory.
 | 
			
		||||
// api_build_tool
 | 
			
		||||
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
 | 
			
		||||
 | 
			
		||||
// @title WireGuard Portal API
 | 
			
		||||
// @title WireGuard Portal SPA-UI API
 | 
			
		||||
// @version 0.0
 | 
			
		||||
// @description WireGuard Portal API - a testing API endpoint
 | 
			
		||||
// @description WireGuard Portal API - UI Endpoints
 | 
			
		||||
 | 
			
		||||
// @contact.name WireGuard Portal Developers
 | 
			
		||||
// @contact.url https://github.com/h44z/wg-portal
 | 
			
		||||
 
 | 
			
		||||
@@ -4,13 +4,14 @@ import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/v0/model"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/v0/model"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
//go:embed frontend_config.js.gotpl
 | 
			
		||||
@@ -63,7 +64,8 @@ func (e configEndpoint) handleConfigJsGet() gin.HandlerFunc {
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				host, port, _ = net.SplitHostPort(parsedReferer.Host)
 | 
			
		||||
			}
 | 
			
		||||
			backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host, port) // override if request comes from frontend started with npm run dev
 | 
			
		||||
			backendUrl = fmt.Sprintf("http://%s:%s/api/v0", host,
 | 
			
		||||
				port) // override if request comes from frontend started with npm run dev
 | 
			
		||||
		}
 | 
			
		||||
		buf := &bytes.Buffer{}
 | 
			
		||||
		err := e.tpl.ExecuteTemplate(buf, "frontend_config.js.gotpl", gin.H{
 | 
			
		||||
@@ -96,6 +98,7 @@ func (e configEndpoint) handleSettingsGet() gin.HandlerFunc {
 | 
			
		||||
			MailLinkOnly:              e.app.Config.Mail.LinkOnly,
 | 
			
		||||
			PersistentConfigSupported: e.app.Config.Advanced.ConfigStoragePath != "",
 | 
			
		||||
			SelfProvisioning:          e.app.Config.Core.SelfProvisioningAllowed,
 | 
			
		||||
			ApiAdminOnly:              e.app.Config.Advanced.ApiAdminOnly,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 {file} binary
 | 
			
		||||
// @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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"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"
 | 
			
		||||
	"net/http"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type userEndpoint struct {
 | 
			
		||||
@@ -27,6 +28,8 @@ func (e userEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenti
 | 
			
		||||
	apiGroup.POST("/new", e.authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
 | 
			
		||||
	apiGroup.GET("/:id/peers", e.authenticator.UserIdMatch("id"), e.handlePeersGet())
 | 
			
		||||
	apiGroup.GET("/:id/stats", e.authenticator.UserIdMatch("id"), e.handleStatsGet())
 | 
			
		||||
	apiGroup.POST("/:id/api/enable", e.authenticator.UserIdMatch("id"), e.handleApiEnablePost())
 | 
			
		||||
	apiGroup.POST("/:id/api/disable", e.authenticator.UserIdMatch("id"), e.handleApiDisablePost())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleAllGet returns a gorm handler function.
 | 
			
		||||
@@ -44,7 +47,8 @@ func (e userEndpoint) handleAllGet() gin.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		users, err := e.app.GetAllUsers(ctx)
 | 
			
		||||
		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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -74,11 +78,12 @@ func (e userEndpoint) handleSingleGet() gin.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		user, err := e.app.GetUser(ctx, domain.UserIdentifier(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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, model.NewUser(user))
 | 
			
		||||
		c.JSON(http.StatusOK, model.NewUser(user, true))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -118,11 +123,12 @@ func (e userEndpoint) handleUpdatePut() gin.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		updateUser, err := e.app.UpdateUser(ctx, model.NewDomainUser(&user))
 | 
			
		||||
		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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, model.NewUser(updateUser))
 | 
			
		||||
		c.JSON(http.StatusOK, model.NewUser(updateUser, false))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -150,11 +156,12 @@ func (e userEndpoint) handleCreatePost() gin.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		newUser, err := e.app.CreateUser(ctx, model.NewDomainUser(&user))
 | 
			
		||||
		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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, model.NewUser(newUser))
 | 
			
		||||
		c.JSON(http.StatusOK, model.NewUser(newUser, false))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -174,13 +181,15 @@ func (e userEndpoint) handlePeersGet() gin.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		interfaceId := Base64UrlDecode(c.Param("id"))
 | 
			
		||||
		if interfaceId == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
 | 
			
		||||
			c.JSON(http.StatusBadRequest,
 | 
			
		||||
				model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		peers, err := e.app.GetUserPeers(ctx, domain.UserIdentifier(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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -204,13 +213,15 @@ func (e userEndpoint) handleStatsGet() gin.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		userId := Base64UrlDecode(c.Param("id"))
 | 
			
		||||
		if userId == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
 | 
			
		||||
			c.JSON(http.StatusBadRequest,
 | 
			
		||||
				model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		stats, err := e.app.GetUserPeerStats(ctx, domain.UserIdentifier(userId))
 | 
			
		||||
		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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@@ -241,10 +252,75 @@ func (e userEndpoint) handleDelete() gin.HandlerFunc {
 | 
			
		||||
 | 
			
		||||
		err := e.app.DeleteUser(ctx, domain.UserIdentifier(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
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Status(http.StatusNoContent)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleApiEnablePost returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID users_handleApiEnablePost
 | 
			
		||||
// @Tags Users
 | 
			
		||||
// @Summary Enable the REST API for the given user.
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} model.User
 | 
			
		||||
// @Failure 400 {object} model.Error
 | 
			
		||||
// @Failure 500 {object} model.Error
 | 
			
		||||
// @Router /user/{id}/api/enable [post]
 | 
			
		||||
func (e userEndpoint) handleApiEnablePost() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		userId := Base64UrlDecode(c.Param("id"))
 | 
			
		||||
		if userId == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest,
 | 
			
		||||
				model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		user, err := e.app.ActivateApi(ctx, domain.UserIdentifier(userId))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusInternalServerError,
 | 
			
		||||
				model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, model.NewUser(user, true))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleApiDisablePost returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID users_handleApiDisablePost
 | 
			
		||||
// @Tags Users
 | 
			
		||||
// @Summary Disable the REST API for the given user.
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} model.User
 | 
			
		||||
// @Failure 400 {object} model.Error
 | 
			
		||||
// @Failure 500 {object} model.Error
 | 
			
		||||
// @Router /user/{id}/api/disable [post]
 | 
			
		||||
func (e userEndpoint) handleApiDisablePost() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		userId := Base64UrlDecode(c.Param("id"))
 | 
			
		||||
		if userId == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest,
 | 
			
		||||
				model.Error{Code: http.StatusInternalServerError, Message: "missing id parameter"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		user, err := e.app.DeactivateApi(ctx, domain.UserIdentifier(userId))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusInternalServerError,
 | 
			
		||||
				model.Error{Code: http.StatusInternalServerError, Message: err.Error()})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, model.NewUser(user, false))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -9,4 +9,5 @@ type Settings struct {
 | 
			
		||||
	MailLinkOnly              bool `json:"MailLinkOnly"`
 | 
			
		||||
	PersistentConfigSupported bool `json:"PersistentConfigSupported"`
 | 
			
		||||
	SelfProvisioning          bool `json:"SelfProvisioning"`
 | 
			
		||||
	ApiAdminOnly              bool `json:"ApiAdminOnly"`
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,37 +25,50 @@ type User struct {
 | 
			
		||||
	Locked         bool   `json:"Locked"`         // if this field is set, the user is locked
 | 
			
		||||
	LockedReason   string `json:"LockedReason"`   // the reason why the user has been locked
 | 
			
		||||
 | 
			
		||||
	ApiToken        string     `json:"ApiToken"`
 | 
			
		||||
	ApiTokenCreated *time.Time `json:"ApiTokenCreated,omitempty"`
 | 
			
		||||
	ApiEnabled      bool       `json:"ApiEnabled"`
 | 
			
		||||
 | 
			
		||||
	// Calculated
 | 
			
		||||
 | 
			
		||||
	PeerCount int `json:"PeerCount"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUser(src *domain.User) *User {
 | 
			
		||||
	return &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,
 | 
			
		||||
func NewUser(src *domain.User, exposeCreds 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
 | 
			
		||||
		ApiTokenCreated: src.ApiTokenCreated,
 | 
			
		||||
		ApiEnabled:      src.IsApiEnabled(),
 | 
			
		||||
 | 
			
		||||
		PeerCount: src.LinkedPeerCount,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if exposeCreds {
 | 
			
		||||
		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])
 | 
			
		||||
		results[i] = *NewUser(&src[i], false)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return results
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										81
									
								
								internal/app/api/v1/handlers/base.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								internal/app/api/v1/handlers/base.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,81 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-contrib/cors"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/core"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/v1/models"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/domain"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Handler interface {
 | 
			
		||||
	GetName() string
 | 
			
		||||
	RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// To compile the API documentation use the
 | 
			
		||||
// api_build_tool
 | 
			
		||||
// command that can be found in the $PROJECT_ROOT/cmd/api_build_tool directory.
 | 
			
		||||
 | 
			
		||||
// @title WireGuard Portal Public API
 | 
			
		||||
// @version 1.0
 | 
			
		||||
// @description The WireGuard Portal REST API enables efficient management of WireGuard VPN configurations through a set of JSON-based endpoints.
 | 
			
		||||
// @description It supports creating and editing peers, interfaces, and user profiles, while also providing role-based access control and auditing.
 | 
			
		||||
// @description This API allows seamless integration with external tools or scripts for automated network configuration and administration.
 | 
			
		||||
 | 
			
		||||
// @license.name MIT
 | 
			
		||||
// @license.url https://github.com/h44z/wg-portal/blob/master/LICENSE.txt
 | 
			
		||||
 | 
			
		||||
// @contact.name WireGuard Portal Project
 | 
			
		||||
// @contact.url https://github.com/h44z/wg-portal
 | 
			
		||||
 | 
			
		||||
// @securityDefinitions.basic BasicAuth
 | 
			
		||||
 | 
			
		||||
// @BasePath /api/v1
 | 
			
		||||
// @query.collection.format multi
 | 
			
		||||
 | 
			
		||||
func NewRestApi(userSource UserSource, handlers ...Handler) core.ApiEndpointSetupFunc {
 | 
			
		||||
	authenticator := &authenticationHandler{
 | 
			
		||||
		userSource: userSource,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return func() (core.ApiVersion, core.GroupSetupFn) {
 | 
			
		||||
		return "v1", func(group *gin.RouterGroup) {
 | 
			
		||||
			group.Use(cors.Default())
 | 
			
		||||
 | 
			
		||||
			// Handler functions
 | 
			
		||||
			for _, h := range handlers {
 | 
			
		||||
				h.RegisterRoutes(group, authenticator)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ParseServiceError(err error) (int, models.Error) {
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return 500, models.Error{
 | 
			
		||||
			Code:    500,
 | 
			
		||||
			Message: "unknown server error",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	code := http.StatusInternalServerError
 | 
			
		||||
	switch {
 | 
			
		||||
	case errors.Is(err, domain.ErrNotFound):
 | 
			
		||||
		code = http.StatusNotFound
 | 
			
		||||
	case errors.Is(err, domain.ErrNoPermission):
 | 
			
		||||
		code = http.StatusForbidden
 | 
			
		||||
	case errors.Is(err, domain.ErrDuplicateEntry):
 | 
			
		||||
		code = http.StatusConflict
 | 
			
		||||
	case errors.Is(err, domain.ErrInvalidData):
 | 
			
		||||
		code = http.StatusBadRequest
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return code, models.Error{
 | 
			
		||||
		Code:    code,
 | 
			
		||||
		Message: err.Error(),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										220
									
								
								internal/app/api/v1/handlers/endpoint_interface.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										220
									
								
								internal/app/api/v1/handlers/endpoint_interface.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,220 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/v1/models"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/domain"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type InterfaceEndpointInterfaceService interface {
 | 
			
		||||
	GetAll(context.Context) ([]domain.Interface, [][]domain.Peer, error)
 | 
			
		||||
	GetById(context.Context, domain.InterfaceIdentifier) (*domain.Interface, []domain.Peer, error)
 | 
			
		||||
	Create(context.Context, *domain.Interface) (*domain.Interface, error)
 | 
			
		||||
	Update(context.Context, domain.InterfaceIdentifier, *domain.Interface) (*domain.Interface, []domain.Peer, error)
 | 
			
		||||
	Delete(context.Context, domain.InterfaceIdentifier) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InterfaceEndpoint struct {
 | 
			
		||||
	interfaces InterfaceEndpointInterfaceService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewInterfaceEndpoint(interfaceService InterfaceEndpointInterfaceService) *InterfaceEndpoint {
 | 
			
		||||
	return &InterfaceEndpoint{
 | 
			
		||||
		interfaces: interfaceService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e InterfaceEndpoint) GetName() string {
 | 
			
		||||
	return "InterfaceEndpoint"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e InterfaceEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
 | 
			
		||||
	apiGroup := g.Group("/interface", authenticator.LoggedIn())
 | 
			
		||||
 | 
			
		||||
	apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
 | 
			
		||||
	apiGroup.GET("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleByIdGet())
 | 
			
		||||
 | 
			
		||||
	apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
 | 
			
		||||
	apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
 | 
			
		||||
	apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleAllGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID interface_handleAllGet
 | 
			
		||||
// @Tags Interfaces
 | 
			
		||||
// @Summary Get all interface records.
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} []models.Interface
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /interface/all [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e InterfaceEndpoint) handleAllGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		allInterfaces, allPeersPerInterface, err := e.interfaces.GetAll(ctx)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewInterfaces(allInterfaces, allPeersPerInterface))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleByIdGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID interfaces_handleByIdGet
 | 
			
		||||
// @Tags Interfaces
 | 
			
		||||
// @Summary Get a specific interface record by its identifier.
 | 
			
		||||
// @Param id path string true "The interface identifier."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.Interface
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /interface/by-id/{id} [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e InterfaceEndpoint) handleByIdGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		iface, interfacePeers, err := e.interfaces.GetById(ctx, domain.InterfaceIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewInterface(iface, interfacePeers))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleCreatePost returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID interfaces_handleCreatePost
 | 
			
		||||
// @Tags Interfaces
 | 
			
		||||
// @Summary Create a new interface record.
 | 
			
		||||
// @Param request body models.Interface true "The interface data."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.Interface
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 409 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /interface/new [post]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e InterfaceEndpoint) handleCreatePost() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		var iface models.Interface
 | 
			
		||||
		err := c.BindJSON(&iface)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		newInterface, err := e.interfaces.Create(ctx, models.NewDomainInterface(&iface))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewInterface(newInterface, nil))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUpdatePut returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID interfaces_handleUpdatePut
 | 
			
		||||
// @Tags Interfaces
 | 
			
		||||
// @Summary Update an interface record.
 | 
			
		||||
// @Param id path string true "The interface identifier."
 | 
			
		||||
// @Param request body models.Interface true "The interface data."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.Interface
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /interface/by-id/{id} [put]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e InterfaceEndpoint) handleUpdatePut() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var iface models.Interface
 | 
			
		||||
		err := c.BindJSON(&iface)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		updatedInterface, updatedInterfacePeers, err := e.interfaces.Update(
 | 
			
		||||
			ctx,
 | 
			
		||||
			domain.InterfaceIdentifier(id),
 | 
			
		||||
			models.NewDomainInterface(&iface),
 | 
			
		||||
		)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewInterface(updatedInterface, updatedInterfacePeers))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleDelete returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID interfaces_handleDelete
 | 
			
		||||
// @Tags Interfaces
 | 
			
		||||
// @Summary Delete the interface record.
 | 
			
		||||
// @Param id path string true "The interface identifier."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 204 "No content if deletion was successful."
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /interface/by-id/{id} [delete]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e InterfaceEndpoint) handleDelete() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := e.interfaces.Delete(ctx, domain.InterfaceIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Status(http.StatusNoContent)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										261
									
								
								internal/app/api/v1/handlers/endpoint_peer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								internal/app/api/v1/handlers/endpoint_peer.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,261 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/v1/models"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/domain"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type PeerService interface {
 | 
			
		||||
	GetForInterface(context.Context, domain.InterfaceIdentifier) ([]domain.Peer, error)
 | 
			
		||||
	GetForUser(context.Context, domain.UserIdentifier) ([]domain.Peer, error)
 | 
			
		||||
	GetById(context.Context, domain.PeerIdentifier) (*domain.Peer, error)
 | 
			
		||||
	Create(context.Context, *domain.Peer) (*domain.Peer, error)
 | 
			
		||||
	Update(context.Context, domain.PeerIdentifier, *domain.Peer) (*domain.Peer, error)
 | 
			
		||||
	Delete(context.Context, domain.PeerIdentifier) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PeerEndpoint struct {
 | 
			
		||||
	peers PeerService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewPeerEndpoint(peerService PeerService) *PeerEndpoint {
 | 
			
		||||
	return &PeerEndpoint{
 | 
			
		||||
		peers: peerService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e PeerEndpoint) GetName() string {
 | 
			
		||||
	return "PeerEndpoint"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e PeerEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
 | 
			
		||||
	apiGroup := g.Group("/peer", authenticator.LoggedIn())
 | 
			
		||||
 | 
			
		||||
	apiGroup.GET("/by-interface/:id", authenticator.LoggedIn(ScopeAdmin), e.handleAllForInterfaceGet())
 | 
			
		||||
	apiGroup.GET("/by-user/:id", authenticator.LoggedIn(), e.handleAllForUserGet())
 | 
			
		||||
	apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
 | 
			
		||||
 | 
			
		||||
	apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
 | 
			
		||||
	apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
 | 
			
		||||
	apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleAllForInterfaceGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID peers_handleAllForInterfaceGet
 | 
			
		||||
// @Tags Peers
 | 
			
		||||
// @Summary Get all peer records for a given WireGuard interface.
 | 
			
		||||
// @Param id path string true "The WireGuard interface identifier."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} []models.Peer
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /peer/by-interface/{id} [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e PeerEndpoint) handleAllForInterfaceGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing interface id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		interfacePeers, err := e.peers.GetForInterface(ctx, domain.InterfaceIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleAllForUserGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID peers_handleAllForUserGet
 | 
			
		||||
// @Tags Peers
 | 
			
		||||
// @Summary Get all peer records for a given user.
 | 
			
		||||
// @Description Normal users can only access their own records. Admins can access all records.
 | 
			
		||||
// @Param id path string true "The user identifier."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} []models.Peer
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /peer/by-user/{id} [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e PeerEndpoint) handleAllForUserGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		interfacePeers, err := e.peers.GetForUser(ctx, domain.UserIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewPeers(interfacePeers))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleByIdGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID peers_handleByIdGet
 | 
			
		||||
// @Tags Peers
 | 
			
		||||
// @Summary Get a specific peer record by its identifier (public key).
 | 
			
		||||
// @Description Normal users can only access their own records. Admins can access all records.
 | 
			
		||||
// @Param id path string true "The peer identifier (public key)."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.Peer
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /peer/by-id/{id} [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e PeerEndpoint) handleByIdGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		peer, err := e.peers.GetById(ctx, domain.PeerIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewPeer(peer))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleCreatePost returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID peers_handleCreatePost
 | 
			
		||||
// @Tags Peers
 | 
			
		||||
// @Summary Create a new peer record.
 | 
			
		||||
// @Description Only admins can create new records.
 | 
			
		||||
// @Param request body models.Peer true "The peer data."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.Peer
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 409 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /peer/new [post]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e PeerEndpoint) handleCreatePost() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		var peer models.Peer
 | 
			
		||||
		err := c.BindJSON(&peer)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		newPeer, err := e.peers.Create(ctx, models.NewDomainPeer(&peer))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewPeer(newPeer))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUpdatePut returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID peers_handleUpdatePut
 | 
			
		||||
// @Tags Peers
 | 
			
		||||
// @Summary Update a peer record.
 | 
			
		||||
// @Description Only admins can update existing records.
 | 
			
		||||
// @Param id path string true "The peer identifier."
 | 
			
		||||
// @Param request body models.Peer true "The peer data."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.Peer
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /peer/by-id/{id} [put]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e PeerEndpoint) handleUpdatePut() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var peer models.Peer
 | 
			
		||||
		err := c.BindJSON(&peer)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		updatedPeer, err := e.peers.Update(ctx, domain.PeerIdentifier(id), models.NewDomainPeer(&peer))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewPeer(updatedPeer))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleDelete returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID peers_handleDelete
 | 
			
		||||
// @Tags Peers
 | 
			
		||||
// @Summary Delete the peer record.
 | 
			
		||||
// @Param id path string true "The peer identifier."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 204 "No content if deletion was successful."
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /peer/by-id/{id} [delete]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e PeerEndpoint) handleDelete() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := e.peers.Delete(ctx, domain.PeerIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Status(http.StatusNoContent)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										195
									
								
								internal/app/api/v1/handlers/endpoint_provisioning.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								internal/app/api/v1/handlers/endpoint_provisioning.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,195 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/v1/models"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/domain"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ProvisioningEndpointProvisioningService interface {
 | 
			
		||||
	GetUserAndPeers(ctx context.Context, userId domain.UserIdentifier, email string) (
 | 
			
		||||
		*domain.User,
 | 
			
		||||
		[]domain.Peer,
 | 
			
		||||
		error,
 | 
			
		||||
	)
 | 
			
		||||
	GetPeerConfig(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
 | 
			
		||||
	GetPeerQrPng(ctx context.Context, peerId domain.PeerIdentifier) ([]byte, error)
 | 
			
		||||
	NewPeer(ctx context.Context, req models.ProvisioningRequest) (*domain.Peer, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ProvisioningEndpoint struct {
 | 
			
		||||
	provisioning ProvisioningEndpointProvisioningService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewProvisioningEndpoint(provisioning ProvisioningEndpointProvisioningService) *ProvisioningEndpoint {
 | 
			
		||||
	return &ProvisioningEndpoint{
 | 
			
		||||
		provisioning: provisioning,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ProvisioningEndpoint) GetName() string {
 | 
			
		||||
	return "ProvisioningEndpoint"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e ProvisioningEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
 | 
			
		||||
	apiGroup := g.Group("/provisioning", authenticator.LoggedIn())
 | 
			
		||||
 | 
			
		||||
	apiGroup.GET("/data/user-info", authenticator.LoggedIn(), e.handleUserInfoGet())
 | 
			
		||||
	apiGroup.GET("/data/peer-config", authenticator.LoggedIn(), e.handlePeerConfigGet())
 | 
			
		||||
	apiGroup.GET("/data/peer-qr", authenticator.LoggedIn(), e.handlePeerQrGet())
 | 
			
		||||
 | 
			
		||||
	apiGroup.POST("/new-peer", authenticator.LoggedIn(), e.handleNewPeerPost())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUserInfoGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID provisioning_handleUserInfoGet
 | 
			
		||||
// @Tags Provisioning
 | 
			
		||||
// @Summary Get information about all peer records for a given user.
 | 
			
		||||
// @Description Normal users can only access their own record. Admins can access all records.
 | 
			
		||||
// @Param UserId query string false "The user identifier that should be queried. If not set, the authenticated user is used."
 | 
			
		||||
// @Param Email query string false "The email address that should be queried. If UserId is set, this is ignored."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.UserInformation
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /provisioning/data/user-info [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e ProvisioningEndpoint) handleUserInfoGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := strings.TrimSpace(c.Query("UserId"))
 | 
			
		||||
		email := strings.TrimSpace(c.Query("Email"))
 | 
			
		||||
 | 
			
		||||
		if id == "" && email == "" {
 | 
			
		||||
			id = string(domain.GetUserInfo(ctx).Id)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		user, peers, err := e.provisioning.GetUserAndPeers(ctx, domain.UserIdentifier(id), email)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewUserInformation(user, peers))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handlePeerConfigGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID provisioning_handlePeerConfigGet
 | 
			
		||||
// @Tags Provisioning
 | 
			
		||||
// @Summary Get the peer configuration in wg-quick format.
 | 
			
		||||
// @Description Normal users can only access their own record. Admins can access all records.
 | 
			
		||||
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
 | 
			
		||||
// @Produce plain
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {string} string "The WireGuard configuration file"
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /provisioning/data/peer-config [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e ProvisioningEndpoint) handlePeerConfigGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := strings.TrimSpace(c.Query("PeerId"))
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		peerConfig, err := e.provisioning.GetPeerConfig(ctx, domain.PeerIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Data(http.StatusOK, "text/plain", peerConfig)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handlePeerQrGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID provisioning_handlePeerQrGet
 | 
			
		||||
// @Tags Provisioning
 | 
			
		||||
// @Summary Get the peer configuration as QR code.
 | 
			
		||||
// @Description Normal users can only access their own record. Admins can access all records.
 | 
			
		||||
// @Param PeerId query string true "The peer identifier (public key) that should be queried."
 | 
			
		||||
// @Produce png
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {file} binary "The WireGuard configuration QR code"
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /provisioning/data/peer-qr [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e ProvisioningEndpoint) handlePeerQrGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := strings.TrimSpace(c.Query("PeerId"))
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing peer id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		peerConfigQrCode, err := e.provisioning.GetPeerQrPng(ctx, domain.PeerIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Data(http.StatusOK, "image/png", peerConfigQrCode)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleNewPeerPost returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID provisioning_handleNewPeerPost
 | 
			
		||||
// @Tags Provisioning
 | 
			
		||||
// @Summary Create a new peer for the given interface and user.
 | 
			
		||||
// @Description Normal users can only create new peers if self provisioning is allowed. Admins can always add new peers.
 | 
			
		||||
// @Param request body models.ProvisioningRequest true "Provisioning request model."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.Peer
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /provisioning/new-peer [post]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e ProvisioningEndpoint) handleNewPeerPost() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		var req models.ProvisioningRequest
 | 
			
		||||
		err := c.BindJSON(&req)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		peer, err := e.provisioning.NewPeer(ctx, req)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewPeer(peer))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										218
									
								
								internal/app/api/v1/handlers/endpoint_user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								internal/app/api/v1/handlers/endpoint_user.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,218 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/v1/models"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/domain"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UserService interface {
 | 
			
		||||
	GetAll(ctx context.Context) ([]domain.User, error)
 | 
			
		||||
	GetById(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
 | 
			
		||||
	Create(ctx context.Context, user *domain.User) (*domain.User, error)
 | 
			
		||||
	Update(ctx context.Context, id domain.UserIdentifier, user *domain.User) (*domain.User, error)
 | 
			
		||||
	Delete(ctx context.Context, id domain.UserIdentifier) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type UserEndpoint struct {
 | 
			
		||||
	users UserService
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewUserEndpoint(userService UserService) *UserEndpoint {
 | 
			
		||||
	return &UserEndpoint{
 | 
			
		||||
		users: userService,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e UserEndpoint) GetName() string {
 | 
			
		||||
	return "UserEndpoint"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e UserEndpoint) RegisterRoutes(g *gin.RouterGroup, authenticator *authenticationHandler) {
 | 
			
		||||
	apiGroup := g.Group("/user", authenticator.LoggedIn())
 | 
			
		||||
 | 
			
		||||
	apiGroup.GET("/all", authenticator.LoggedIn(ScopeAdmin), e.handleAllGet())
 | 
			
		||||
	apiGroup.GET("/by-id/:id", authenticator.LoggedIn(), e.handleByIdGet())
 | 
			
		||||
	apiGroup.POST("/new", authenticator.LoggedIn(ScopeAdmin), e.handleCreatePost())
 | 
			
		||||
	apiGroup.PUT("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleUpdatePut())
 | 
			
		||||
	apiGroup.DELETE("/by-id/:id", authenticator.LoggedIn(ScopeAdmin), e.handleDelete())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleAllGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID users_handleAllGet
 | 
			
		||||
// @Tags Users
 | 
			
		||||
// @Summary Get all user records.
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} []models.User
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /user/all [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e UserEndpoint) handleAllGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		users, err := e.users.GetAll(ctx)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewUsers(users))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleByIdGet returns a gorm Handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID users_handleByIdGet
 | 
			
		||||
// @Tags Users
 | 
			
		||||
// @Summary Get a specific user record by its internal identifier.
 | 
			
		||||
// @Description Normal users can only access their own record. Admins can access all records.
 | 
			
		||||
// @Param id path string true "The user identifier."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.User
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /user/by-id/{id} [get]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e UserEndpoint) handleByIdGet() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		user, err := e.users.GetById(ctx, domain.UserIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewUser(user, true))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleCreatePost returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID users_handleCreatePost
 | 
			
		||||
// @Tags Users
 | 
			
		||||
// @Summary Create a new user record.
 | 
			
		||||
// @Description Only admins can create new records.
 | 
			
		||||
// @Param request body models.User true "The user data."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.User
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 409 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /user/new [post]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e UserEndpoint) handleCreatePost() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		var user models.User
 | 
			
		||||
		err := c.BindJSON(&user)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		newUser, err := e.users.Create(ctx, models.NewDomainUser(&user))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewUser(newUser, true))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleUpdatePut returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID users_handleUpdatePut
 | 
			
		||||
// @Tags Users
 | 
			
		||||
// @Summary Update a user record.
 | 
			
		||||
// @Description Only admins can update existing records.
 | 
			
		||||
// @Param id path string true "The user identifier."
 | 
			
		||||
// @Param request body models.User true "The user data."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {object} models.User
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /user/by-id/{id} [put]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e UserEndpoint) handleUpdatePut() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var user models.User
 | 
			
		||||
		err := c.BindJSON(&user)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: err.Error()})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		updateUser, err := e.users.Update(ctx, domain.UserIdentifier(id), models.NewDomainUser(&user))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.JSON(http.StatusOK, models.NewUser(updateUser, true))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleDelete returns a gorm handler function.
 | 
			
		||||
//
 | 
			
		||||
// @ID users_handleDelete
 | 
			
		||||
// @Tags Users
 | 
			
		||||
// @Summary Delete the user record.
 | 
			
		||||
// @Param id path string true "The user identifier."
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 204 "No content if deletion was successful."
 | 
			
		||||
// @Failure 400 {object} models.Error
 | 
			
		||||
// @Failure 401 {object} models.Error
 | 
			
		||||
// @Failure 403 {object} models.Error
 | 
			
		||||
// @Failure 404 {object} models.Error
 | 
			
		||||
// @Failure 500 {object} models.Error
 | 
			
		||||
// @Router /user/by-id/{id} [delete]
 | 
			
		||||
// @Security BasicAuth
 | 
			
		||||
func (e UserEndpoint) handleDelete() gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		ctx := domain.SetUserInfoFromGin(c)
 | 
			
		||||
 | 
			
		||||
		id := c.Param("id")
 | 
			
		||||
		if id == "" {
 | 
			
		||||
			c.JSON(http.StatusBadRequest, models.Error{Code: http.StatusBadRequest, Message: "missing user id"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := e.users.Delete(ctx, domain.UserIdentifier(id))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			c.JSON(ParseServiceError(err))
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Status(http.StatusNoContent)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										92
									
								
								internal/app/api/v1/handlers/middleware_authentication.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								internal/app/api/v1/handlers/middleware_authentication.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app/api/v0/model"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/domain"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Scope string
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	ScopeAdmin Scope = "ADMIN" // Admin scope contains all other scopes
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type UserSource interface {
 | 
			
		||||
	GetUser(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type authenticationHandler struct {
 | 
			
		||||
	userSource UserSource
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// LoggedIn checks if a user is logged in. If scopes are given, they are validated as well.
 | 
			
		||||
func (h authenticationHandler) LoggedIn(scopes ...Scope) gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		username, password, ok := c.Request.BasicAuth()
 | 
			
		||||
		if !ok || username == "" || password == "" {
 | 
			
		||||
			// Abort the request with the appropriate error code
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "missing credentials"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// check if user exists in DB
 | 
			
		||||
 | 
			
		||||
		ctx := domain.SetUserInfo(c.Request.Context(), domain.SystemAdminContextUserInfo())
 | 
			
		||||
		user, err := h.userSource.GetUser(ctx, domain.UserIdentifier(username))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			// Abort the request with the appropriate error code
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// validate API token
 | 
			
		||||
		if err := user.CheckApiToken(password); err != nil {
 | 
			
		||||
			// Abort the request with the appropriate error code
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			c.JSON(http.StatusUnauthorized, model.Error{Code: http.StatusUnauthorized, Message: "invalid credentials"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !UserHasScopes(user, scopes...) {
 | 
			
		||||
			// Abort the request with the appropriate error code
 | 
			
		||||
			c.Abort()
 | 
			
		||||
			c.JSON(http.StatusForbidden, model.Error{Code: http.StatusForbidden, Message: "not enough permissions"})
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		c.Set(domain.CtxUserInfo, &domain.ContextUserInfo{
 | 
			
		||||
			Id:      user.Identifier,
 | 
			
		||||
			IsAdmin: user.IsAdmin,
 | 
			
		||||
		})
 | 
			
		||||
 | 
			
		||||
		// Continue down the chain to Handler etc
 | 
			
		||||
		c.Next()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UserHasScopes(user *domain.User, scopes ...Scope) bool {
 | 
			
		||||
	// No scopes give, so the check should succeed
 | 
			
		||||
	if len(scopes) == 0 {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// check if user has admin scope
 | 
			
		||||
	if user.IsAdmin {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if admin scope is required
 | 
			
		||||
	for _, scope := range scopes {
 | 
			
		||||
		if scope == ScopeAdmin {
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								internal/app/api/v1/models/model_options.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								internal/app/api/v1/models/model_options.go
									
									
									
									
									
										Normal 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,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								internal/app/api/v1/models/models.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								internal/app/api/v1/models/models.go
									
									
									
									
									
										Normal 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.
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										201
									
								
								internal/app/api/v1/models/models_interface.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								internal/app/api/v1/models/models_interface.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										195
									
								
								internal/app/api/v1/models/models_peer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								internal/app/api/v1/models/models_peer.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										75
									
								
								internal/app/api/v1/models/models_provisioning.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								internal/app/api/v1/models/models_provisioning.go
									
									
									
									
									
										Normal 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"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										125
									
								
								internal/app/api/v1/models/models_user.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								internal/app/api/v1/models/models_user.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
@@ -22,9 +22,19 @@ type App struct {
 | 
			
		||||
	StatisticsCollector
 | 
			
		||||
	ConfigFileManager
 | 
			
		||||
	MailManager
 | 
			
		||||
	ApiV1Manager
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator, users UserManager, wireGuard WireGuardManager, stats StatisticsCollector, cfgFiles ConfigFileManager, mailer MailManager) (*App, error) {
 | 
			
		||||
func New(
 | 
			
		||||
	cfg *config.Config,
 | 
			
		||||
	bus evbus.MessageBus,
 | 
			
		||||
	authenticator Authenticator,
 | 
			
		||||
	users UserManager,
 | 
			
		||||
	wireGuard WireGuardManager,
 | 
			
		||||
	stats StatisticsCollector,
 | 
			
		||||
	cfgFiles ConfigFileManager,
 | 
			
		||||
	mailer MailManager,
 | 
			
		||||
) (*App, error) {
 | 
			
		||||
 | 
			
		||||
	a := &App{
 | 
			
		||||
		Config: cfg,
 | 
			
		||||
@@ -60,7 +70,7 @@ func New(cfg *config.Config, bus evbus.MessageBus, authenticator Authenticator,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (a *App) Startup(ctx context.Context) error {
 | 
			
		||||
	
 | 
			
		||||
 | 
			
		||||
	a.UserManager.StartBackgroundJobs(ctx)
 | 
			
		||||
	a.StatisticsCollector.StartBackgroundJobs(ctx)
 | 
			
		||||
	a.WireGuardManager.StartBackgroundJobs(ctx)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
package app
 | 
			
		||||
 | 
			
		||||
const TopicUserCreated = "user:created"
 | 
			
		||||
const TopicUserApiEnabled = "user:api:enabled"
 | 
			
		||||
const TopicUserApiDisabled = "user:api:disabled"
 | 
			
		||||
const TopicUserRegistered = "user:registered"
 | 
			
		||||
const TopicUserDisabled = "user:disabled"
 | 
			
		||||
const TopicUserEnabled = "user:enabled"
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,9 @@ package app
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/domain"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/domain"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Authenticator interface {
 | 
			
		||||
@@ -23,6 +24,8 @@ type UserManager interface {
 | 
			
		||||
	UpdateUser(ctx context.Context, user *domain.User) (*domain.User, error)
 | 
			
		||||
	CreateUser(ctx context.Context, user *domain.User) (*domain.User, error)
 | 
			
		||||
	DeleteUser(ctx context.Context, id domain.UserIdentifier) error
 | 
			
		||||
	ActivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
 | 
			
		||||
	DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type WireGuardManager interface {
 | 
			
		||||
@@ -43,7 +46,11 @@ type WireGuardManager interface {
 | 
			
		||||
	PreparePeer(ctx context.Context, id domain.InterfaceIdentifier) (*domain.Peer, error)
 | 
			
		||||
	GetPeer(ctx context.Context, id domain.PeerIdentifier) (*domain.Peer, error)
 | 
			
		||||
	CreatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
 | 
			
		||||
	CreateMultiplePeers(ctx context.Context, id domain.InterfaceIdentifier, r *domain.PeerCreationRequest) ([]domain.Peer, error)
 | 
			
		||||
	CreateMultiplePeers(
 | 
			
		||||
		ctx context.Context,
 | 
			
		||||
		id domain.InterfaceIdentifier,
 | 
			
		||||
		r *domain.PeerCreationRequest,
 | 
			
		||||
	) ([]domain.Peer, error)
 | 
			
		||||
	UpdatePeer(ctx context.Context, p *domain.Peer) (*domain.Peer, error)
 | 
			
		||||
	DeletePeer(ctx context.Context, id domain.PeerIdentifier) error
 | 
			
		||||
	ApplyPeerDefaults(ctx context.Context, in *domain.Interface) error
 | 
			
		||||
@@ -63,3 +70,7 @@ type ConfigFileManager interface {
 | 
			
		||||
type MailManager interface {
 | 
			
		||||
	SendPeerEmail(ctx context.Context, linkOnly bool, peers ...domain.PeerIdentifier) error
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ApiV1Manager interface {
 | 
			
		||||
	ApiV1GetUsers(ctx context.Context) ([]domain.User, error)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@ import (
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/google/uuid"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app"
 | 
			
		||||
 | 
			
		||||
	"github.com/h44z/wg-portal/internal"
 | 
			
		||||
@@ -101,7 +102,7 @@ func (m Manager) GetUser(ctx context.Context, id domain.UserIdentifier) (*domain
 | 
			
		||||
 | 
			
		||||
	user, err := m.users.GetUser(ctx, id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to load peer %s: %w", id, err)
 | 
			
		||||
		return nil, fmt.Errorf("unable to load user %s: %w", id, err)
 | 
			
		||||
	}
 | 
			
		||||
	peers, _ := m.peers.GetUserPeers(ctx, id) // ignore error, list will be empty in error case
 | 
			
		||||
 | 
			
		||||
@@ -110,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
 | 
			
		||||
@@ -193,7 +212,7 @@ func (m Manager) CreateUser(ctx context.Context, user *domain.User) (*domain.Use
 | 
			
		||||
		return nil, fmt.Errorf("unable to load existing user %s: %w", user.Identifier, err)
 | 
			
		||||
	}
 | 
			
		||||
	if existingUser != nil {
 | 
			
		||||
		return nil, fmt.Errorf("user %s already exists", user.Identifier)
 | 
			
		||||
		return nil, errors.Join(fmt.Errorf("user %s already exists", user.Identifier), domain.ErrDuplicateEntry)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := m.validateCreation(ctx, user); err != nil {
 | 
			
		||||
@@ -240,6 +259,59 @@ func (m Manager) DeleteUser(ctx context.Context, id domain.UserIdentifier) error
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Manager) ActivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
 | 
			
		||||
	user, err := m.users.GetUser(ctx, id)
 | 
			
		||||
	if err != nil && !errors.Is(err, domain.ErrNotFound) {
 | 
			
		||||
		return nil, fmt.Errorf("unable to find user %s: %w", id, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := m.validateApiChange(ctx, user); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	now := time.Now()
 | 
			
		||||
	user.ApiToken = uuid.New().String()
 | 
			
		||||
	user.ApiTokenCreated = &now
 | 
			
		||||
 | 
			
		||||
	err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
 | 
			
		||||
		user.CopyCalculatedAttributes(u)
 | 
			
		||||
		return user, nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("update failure: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.bus.Publish(app.TopicUserApiEnabled, user)
 | 
			
		||||
 | 
			
		||||
	return user, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Manager) DeactivateApi(ctx context.Context, id domain.UserIdentifier) (*domain.User, error) {
 | 
			
		||||
	user, err := m.users.GetUser(ctx, id)
 | 
			
		||||
	if err != nil && !errors.Is(err, domain.ErrNotFound) {
 | 
			
		||||
		return nil, fmt.Errorf("unable to find user %s: %w", id, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := m.validateApiChange(ctx, user); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	user.ApiToken = ""
 | 
			
		||||
	user.ApiTokenCreated = nil
 | 
			
		||||
 | 
			
		||||
	err = m.users.SaveUser(ctx, user.Identifier, func(u *domain.User) (*domain.User, error) {
 | 
			
		||||
		user.CopyCalculatedAttributes(u)
 | 
			
		||||
		return user, nil
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("update failure: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m.bus.Publish(app.TopicUserApiDisabled, user)
 | 
			
		||||
 | 
			
		||||
	return user, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Manager) validateModifications(ctx context.Context, old, new *domain.User) error {
 | 
			
		||||
	currentUser := domain.GetUserInfo(ctx)
 | 
			
		||||
 | 
			
		||||
@@ -248,27 +320,27 @@ func (m Manager) validateModifications(ctx context.Context, old, new *domain.Use
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := old.EditAllowed(new); err != nil {
 | 
			
		||||
		return fmt.Errorf("no access: %w", err)
 | 
			
		||||
		return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := old.CanChangePassword(); err != nil && string(new.Password) != "" {
 | 
			
		||||
		return fmt.Errorf("no access: %w", err)
 | 
			
		||||
		return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if currentUser.Id == old.Identifier && old.IsAdmin && !new.IsAdmin {
 | 
			
		||||
		return fmt.Errorf("cannot remove own admin rights")
 | 
			
		||||
		return fmt.Errorf("cannot remove own admin rights: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if currentUser.Id == old.Identifier && new.IsDisabled() {
 | 
			
		||||
		return fmt.Errorf("cannot disable own user")
 | 
			
		||||
		return fmt.Errorf("cannot disable own user: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if currentUser.Id == old.Identifier && new.IsLocked() {
 | 
			
		||||
		return fmt.Errorf("cannot lock own user")
 | 
			
		||||
		return fmt.Errorf("cannot lock own user: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if old.Source != new.Source {
 | 
			
		||||
		return fmt.Errorf("cannot change user source")
 | 
			
		||||
		return fmt.Errorf("cannot change user source: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -282,19 +354,32 @@ func (m Manager) validateCreation(ctx context.Context, new *domain.User) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if new.Identifier == "" {
 | 
			
		||||
		return fmt.Errorf("invalid user identifier")
 | 
			
		||||
		return fmt.Errorf("invalid user identifier: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if new.Identifier == "all" { // the all user identifier collides with the rest api routes
 | 
			
		||||
		return fmt.Errorf("reserved user identifier")
 | 
			
		||||
	if new.Identifier == "all" { // the 'all' user identifier collides with the rest api routes
 | 
			
		||||
		return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if new.Identifier == "new" { // the 'new' user identifier collides with the rest api routes
 | 
			
		||||
		return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if new.Identifier == "id" { // the 'id' user identifier collides with the rest api routes
 | 
			
		||||
		return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if new.Identifier == domain.CtxSystemAdminId || new.Identifier == domain.CtxUnknownUserId {
 | 
			
		||||
		return fmt.Errorf("reserved user identifier: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if new.Source != domain.UserSourceDatabase {
 | 
			
		||||
		return fmt.Errorf("invalid user source: %s", new.Source)
 | 
			
		||||
		return fmt.Errorf("invalid user source: %s, only %s is allowed: %w",
 | 
			
		||||
			new.Source, domain.UserSourceDatabase, domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if string(new.Password) == "" {
 | 
			
		||||
		return fmt.Errorf("invalid password")
 | 
			
		||||
		return fmt.Errorf("invalid password: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -304,15 +389,25 @@ func (m Manager) validateDeletion(ctx context.Context, del *domain.User) error {
 | 
			
		||||
	currentUser := domain.GetUserInfo(ctx)
 | 
			
		||||
 | 
			
		||||
	if !currentUser.IsAdmin {
 | 
			
		||||
		return fmt.Errorf("insufficient permissions")
 | 
			
		||||
		return domain.ErrNoPermission
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := del.DeleteAllowed(); err != nil {
 | 
			
		||||
		return fmt.Errorf("no access: %w", err)
 | 
			
		||||
		return errors.Join(fmt.Errorf("no access: %w", err), domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if currentUser.Id == del.Identifier {
 | 
			
		||||
		return fmt.Errorf("cannot delete own user")
 | 
			
		||||
		return fmt.Errorf("cannot delete own user: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m Manager) validateApiChange(ctx context.Context, user *domain.User) error {
 | 
			
		||||
	currentUser := domain.GetUserInfo(ctx)
 | 
			
		||||
 | 
			
		||||
	if currentUser.Id != user.Identifier {
 | 
			
		||||
		return fmt.Errorf("cannot change API access of user: %w", domain.ErrNoPermission)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -357,7 +357,7 @@ func (m Manager) CreateInterface(ctx context.Context, in *domain.Interface) (*do
 | 
			
		||||
		return nil, fmt.Errorf("unable to load existing interface %s: %w", in.Identifier, err)
 | 
			
		||||
	}
 | 
			
		||||
	if existingInterface != nil {
 | 
			
		||||
		return nil, fmt.Errorf("interface %s already exists", in.Identifier)
 | 
			
		||||
		return nil, fmt.Errorf("interface %s already exists: %w", in.Identifier, domain.ErrDuplicateEntry)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := m.validateInterfaceCreation(ctx, existingInterface, in); err != nil {
 | 
			
		||||
@@ -825,6 +825,13 @@ func (m Manager) validateInterfaceCreation(ctx context.Context, old, new *domain
 | 
			
		||||
		return fmt.Errorf("insufficient permissions")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// validate public key if it is set
 | 
			
		||||
	if new.PublicKey != "" && new.PrivateKey != "" {
 | 
			
		||||
		if domain.PublicKeyFromPrivateKey(new.PrivateKey) != new.PublicKey {
 | 
			
		||||
			return fmt.Errorf("invalid public key for given privatekey: %w", domain.ErrInvalidData)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/h44z/wg-portal/internal"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/app"
 | 
			
		||||
	"github.com/h44z/wg-portal/internal/domain"
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
@@ -34,9 +33,9 @@ func (m Manager) CreateDefaultPeer(ctx context.Context, userId domain.UserIdenti
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		peer.UserIdentifier = userId
 | 
			
		||||
		peer.DisplayName = fmt.Sprintf("Default Peer %s", internal.TruncateString(string(peer.Identifier), 8))
 | 
			
		||||
		peer.Notes = fmt.Sprintf("Default peer created for user %s", userId)
 | 
			
		||||
		peer.AutomaticallyCreated = true
 | 
			
		||||
		peer.GenerateDisplayName("Default")
 | 
			
		||||
 | 
			
		||||
		newPeers = append(newPeers, *peer)
 | 
			
		||||
	}
 | 
			
		||||
@@ -108,7 +107,6 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
 | 
			
		||||
		ExtraAllowedIPsStr:  "",
 | 
			
		||||
		PresharedKey:        pk,
 | 
			
		||||
		PersistentKeepalive: domain.NewConfigOption(iface.PeerDefPersistentKeepalive, true),
 | 
			
		||||
		DisplayName:         fmt.Sprintf("Peer %s", internal.TruncateString(string(peerId), 8)),
 | 
			
		||||
		Identifier:          peerId,
 | 
			
		||||
		UserIdentifier:      currentUser.Id,
 | 
			
		||||
		InterfaceIdentifier: iface.Identifier,
 | 
			
		||||
@@ -132,6 +130,7 @@ func (m Manager) PreparePeer(ctx context.Context, id domain.InterfaceIdentifier)
 | 
			
		||||
			PostDown:          domain.NewConfigOption(iface.PeerDefPostDown, true),
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	freshPeer.GenerateDisplayName("")
 | 
			
		||||
 | 
			
		||||
	return freshPeer, nil
 | 
			
		||||
}
 | 
			
		||||
@@ -159,7 +158,7 @@ func (m Manager) CreatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
 | 
			
		||||
		return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
 | 
			
		||||
	}
 | 
			
		||||
	if existingPeer != nil {
 | 
			
		||||
		return nil, fmt.Errorf("peer %s already exists", peer.Identifier)
 | 
			
		||||
		return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := m.validatePeerCreation(ctx, existingPeer, peer); err != nil {
 | 
			
		||||
@@ -234,6 +233,15 @@ func (m Manager) UpdatePeer(ctx context.Context, peer *domain.Peer) (*domain.Pee
 | 
			
		||||
	if existingPeer.Identifier != domain.PeerIdentifier(peer.Interface.PublicKey) {
 | 
			
		||||
		peer.Identifier = domain.PeerIdentifier(peer.Interface.PublicKey) // set new identifier
 | 
			
		||||
 | 
			
		||||
		// check for already existing peer with new identifier
 | 
			
		||||
		duplicatePeer, err := m.db.GetPeer(ctx, peer.Identifier)
 | 
			
		||||
		if err != nil && !errors.Is(err, domain.ErrNotFound) {
 | 
			
		||||
			return nil, fmt.Errorf("unable to load existing peer %s: %w", peer.Identifier, err)
 | 
			
		||||
		}
 | 
			
		||||
		if duplicatePeer != nil {
 | 
			
		||||
			return nil, fmt.Errorf("peer %s already exists: %w", peer.Identifier, domain.ErrDuplicateEntry)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// delete old peer
 | 
			
		||||
		err = m.DeletePeer(ctx, existingPeer.Identifier)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
@@ -431,7 +439,7 @@ func (m Manager) validatePeerModifications(ctx context.Context, old, new *domain
 | 
			
		||||
	currentUser := domain.GetUserInfo(ctx)
 | 
			
		||||
 | 
			
		||||
	if !currentUser.IsAdmin {
 | 
			
		||||
		return fmt.Errorf("insufficient permissions")
 | 
			
		||||
		return domain.ErrNoPermission
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -441,11 +449,16 @@ func (m Manager) validatePeerCreation(ctx context.Context, old, new *domain.Peer
 | 
			
		||||
	currentUser := domain.GetUserInfo(ctx)
 | 
			
		||||
 | 
			
		||||
	if new.Identifier == "" {
 | 
			
		||||
		return fmt.Errorf("invalid peer identifier")
 | 
			
		||||
		return fmt.Errorf("invalid peer identifier: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !currentUser.IsAdmin {
 | 
			
		||||
		return fmt.Errorf("insufficient permissions")
 | 
			
		||||
		return domain.ErrNoPermission
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err := m.db.GetInterface(ctx, new.InterfaceIdentifier)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("invalid interface: %w", domain.ErrInvalidData)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
@@ -455,7 +468,7 @@ func (m Manager) validatePeerDeletion(ctx context.Context, del *domain.Peer) err
 | 
			
		||||
	currentUser := domain.GetUserInfo(ctx)
 | 
			
		||||
 | 
			
		||||
	if !currentUser.IsAdmin {
 | 
			
		||||
		return fmt.Errorf("insufficient permissions")
 | 
			
		||||
		return domain.ErrNoPermission
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,7 @@ type Config struct {
 | 
			
		||||
		ExpiryCheckInterval time.Duration `yaml:"expiry_check_interval"`
 | 
			
		||||
		RulePrioOffset      int           `yaml:"rule_prio_offset"`
 | 
			
		||||
		RouteTableOffset    int           `yaml:"route_table_offset"`
 | 
			
		||||
		ApiAdminOnly        bool          `yaml:"api_admin_only"` // if true, only admin users can access the API
 | 
			
		||||
	} `yaml:"advanced"`
 | 
			
		||||
 | 
			
		||||
	Statistics struct {
 | 
			
		||||
@@ -126,6 +127,7 @@ func defaultConfig() *Config {
 | 
			
		||||
	cfg.Advanced.ExpiryCheckInterval = 15 * time.Minute
 | 
			
		||||
	cfg.Advanced.RulePrioOffset = 20000
 | 
			
		||||
	cfg.Advanced.RouteTableOffset = 20000
 | 
			
		||||
	cfg.Advanced.ApiAdminOnly = true
 | 
			
		||||
 | 
			
		||||
	cfg.Statistics.UsePingChecks = true
 | 
			
		||||
	cfg.Statistics.PingCheckWorkers = 10
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ package domain
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/sirupsen/logrus"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
@@ -28,6 +29,7 @@ func (u *ContextUserInfo) UserId() string {
 | 
			
		||||
	return string(u.Id)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DefaultContextUserInfo returns a default context user info.
 | 
			
		||||
func DefaultContextUserInfo() *ContextUserInfo {
 | 
			
		||||
	return &ContextUserInfo{
 | 
			
		||||
		Id:      CtxUnknownUserId,
 | 
			
		||||
@@ -35,6 +37,7 @@ func DefaultContextUserInfo() *ContextUserInfo {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SystemAdminContextUserInfo returns a context user info for the system admin.
 | 
			
		||||
func SystemAdminContextUserInfo() *ContextUserInfo {
 | 
			
		||||
	return &ContextUserInfo{
 | 
			
		||||
		Id:      CtxSystemAdminId,
 | 
			
		||||
@@ -42,6 +45,7 @@ func SystemAdminContextUserInfo() *ContextUserInfo {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetUserInfoFromGin sets the user info from the gin context to the request context.
 | 
			
		||||
func SetUserInfoFromGin(c *gin.Context) context.Context {
 | 
			
		||||
	ginUserInfo, exists := c.Get(CtxUserInfo)
 | 
			
		||||
 | 
			
		||||
@@ -56,11 +60,13 @@ func SetUserInfoFromGin(c *gin.Context) context.Context {
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetUserInfo sets the user info in the context.
 | 
			
		||||
func SetUserInfo(ctx context.Context, info *ContextUserInfo) context.Context {
 | 
			
		||||
	ctx = context.WithValue(ctx, CtxUserInfo, info)
 | 
			
		||||
	return ctx
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetUserInfo returns the user info from the context.
 | 
			
		||||
func GetUserInfo(ctx context.Context) *ContextUserInfo {
 | 
			
		||||
	rawInfo := ctx.Value(CtxUserInfo)
 | 
			
		||||
	if rawInfo == nil {
 | 
			
		||||
@@ -74,6 +80,8 @@ func GetUserInfo(ctx context.Context) *ContextUserInfo {
 | 
			
		||||
	return DefaultContextUserInfo()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ValidateUserAccessRights checks if the current user has access rights to the requested user.
 | 
			
		||||
// If the user is an admin, access is granted.
 | 
			
		||||
func ValidateUserAccessRights(ctx context.Context, requiredUser UserIdentifier) error {
 | 
			
		||||
	sessionUser := GetUserInfo(ctx)
 | 
			
		||||
 | 
			
		||||
@@ -86,9 +94,10 @@ func ValidateUserAccessRights(ctx context.Context, requiredUser UserIdentifier)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logrus.Warnf("insufficient permissions for %s (want %s), stack: %s", sessionUser.Id, requiredUser, GetStackTrace())
 | 
			
		||||
	return fmt.Errorf("insufficient permissions")
 | 
			
		||||
	return ErrNoPermission
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ValidateAdminAccessRights checks if the current user has admin access rights.
 | 
			
		||||
func ValidateAdminAccessRights(ctx context.Context) error {
 | 
			
		||||
	sessionUser := GetUserInfo(ctx)
 | 
			
		||||
 | 
			
		||||
@@ -97,5 +106,5 @@ func ValidateAdminAccessRights(ctx context.Context) error {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logrus.Warnf("insufficient admin permissions for %s, stack: %s", sessionUser.Id, GetStackTrace())
 | 
			
		||||
	return fmt.Errorf("insufficient permissions")
 | 
			
		||||
	return ErrNoPermission
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,9 @@ import (
 | 
			
		||||
 | 
			
		||||
var ErrNotFound = errors.New("record not found")
 | 
			
		||||
var ErrNotUnique = errors.New("record not unique")
 | 
			
		||||
var ErrNoPermission = errors.New("no permission")
 | 
			
		||||
var ErrDuplicateEntry = errors.New("duplicate entry")
 | 
			
		||||
var ErrInvalidData = errors.New("invalid data")
 | 
			
		||||
 | 
			
		||||
// GetStackTrace returns a stack trace of the current goroutine. The stack trace has at most 1024 bytes.
 | 
			
		||||
func GetStackTrace() string {
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,13 @@ func (p *Peer) ApplyInterfaceDefaults(in *Interface) {
 | 
			
		||||
	p.Interface.PostDown.TrySetValue(in.PeerDefPostDown)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Peer) GenerateDisplayName(prefix string) {
 | 
			
		||||
	if prefix != "" {
 | 
			
		||||
		prefix = fmt.Sprintf("%s ", strings.TrimSpace(prefix)) // add a space after the prefix
 | 
			
		||||
	}
 | 
			
		||||
	p.DisplayName = fmt.Sprintf("%sPeer %s", prefix, internal.TruncateString(string(p.Identifier), 8))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type PeerInterfaceConfig struct {
 | 
			
		||||
	KeyPair // private/public Key of the peer
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
package domain
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"crypto/subtle"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
@@ -42,6 +43,10 @@ type User struct {
 | 
			
		||||
	Locked         *time.Time    `gorm:"index;column:locked"` // if this field is set, the user is locked and can no longer login (WireGuard peers still can connect)
 | 
			
		||||
	LockedReason   string        // the reason why the user has been locked
 | 
			
		||||
 | 
			
		||||
	// API token for REST API access
 | 
			
		||||
	ApiToken        string `form:"api_token" binding:"omitempty"`
 | 
			
		||||
	ApiTokenCreated *time.Time
 | 
			
		||||
 | 
			
		||||
	LinkedPeerCount int `gorm:"-"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -56,6 +61,14 @@ func (u *User) IsLocked() bool {
 | 
			
		||||
	return u.Locked != nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *User) IsApiEnabled() bool {
 | 
			
		||||
	if u.ApiToken != "" {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *User) CanChangePassword() error {
 | 
			
		||||
	if u.Source == UserSourceDatabase {
 | 
			
		||||
		return nil
 | 
			
		||||
@@ -115,6 +128,18 @@ func (u *User) CheckPassword(password string) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *User) CheckApiToken(token string) error {
 | 
			
		||||
	if !u.IsApiEnabled() {
 | 
			
		||||
		return errors.New("api access disabled")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if res := subtle.ConstantTimeCompare([]byte(u.ApiToken), []byte(token)); res != 1 {
 | 
			
		||||
		return errors.New("wrong token")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (u *User) HashPassword() error {
 | 
			
		||||
	if u.Password == "" {
 | 
			
		||||
		return nil // nothing to hash
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user