From 5e5f0de0e2ff0e5f6871b64739a2b6238271af19 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 4 Jan 2023 15:36:56 +1000 Subject: [PATCH] - Added upstream objects - Renamed host templates to nginx templates - Generate upstream templates - Better nginx error reporting when reloading - Use tparse for golang test reporting --- backend/embed/api_docs/api.swagger.json | 49 ++- .../components/NginxTemplateList.json | 40 +++ ...teObject.json => NginxTemplateObject.json} | 8 +- ...ostTemplateList.json => UpstreamList.json} | 4 +- .../api_docs/components/UpstreamObject.json | 133 ++++++++ backend/embed/api_docs/paths/hosts/get.json | 3 +- .../api_docs/paths/hosts/hostID/get.json | 3 +- .../api_docs/paths/hosts/hostID/put.json | 3 +- backend/embed/api_docs/paths/hosts/post.json | 3 +- .../get.json | 10 +- .../post.json | 14 +- .../templateID}/delete.json | 12 +- .../templateID}/get.json | 14 +- .../templateID}/put.json | 18 +- .../embed/api_docs/paths/streams/post.json | 12 +- .../embed/api_docs/paths/upstreams/get.json | 285 ++++++++++++++++++ .../embed/api_docs/paths/upstreams/post.json | 80 +++++ .../paths/upstreams/upstreamID/delete.json | 58 ++++ .../paths/upstreams/upstreamID/get.json | 81 +++++ .../20201013035318_initial_schema.sql | 43 ++- .../20201013035839_initial_data.sql | 171 ++++++++++- backend/internal/acme/acmesh.go | 2 +- backend/internal/acme/acmesh_test.go | 9 - backend/internal/api/handler/hosts.go | 32 +- .../{host_templates.go => nginx_templates.go} | 80 ++--- backend/internal/api/handler/schema.go | 6 +- backend/internal/api/handler/upstreams.go | 129 ++++++++ backend/internal/api/router.go | 33 +- backend/internal/api/schema/create_host.go | 7 +- ...t_template.go => create_nginx_template.go} | 10 +- .../internal/api/schema/create_upstream.go | 73 +++++ backend/internal/api/schema/update_host.go | 2 +- ...t_template.go => update_nginx_template.go} | 2 +- backend/internal/config/folders.go | 1 + .../internal/entity/certificate/methods.go | 2 +- backend/internal/entity/certificate/model.go | 9 +- .../entity/certificateauthority/methods.go | 2 +- .../internal/entity/dnsprovider/methods.go | 2 +- backend/internal/entity/host/methods.go | 11 +- backend/internal/entity/host/model.go | 40 +-- backend/internal/entity/host/template.go | 13 +- .../internal/entity/nginxtemplate/filters.go | 25 ++ .../methods.go | 4 +- .../{hosttemplate => nginxtemplate}/model.go | 8 +- .../internal/entity/nginxtemplate/structs.go | 15 + backend/internal/entity/setting/methods.go | 2 +- backend/internal/entity/stream/methods.go | 2 +- .../{hosttemplate => upstream}/filters.go | 2 +- backend/internal/entity/upstream/methods.go | 162 ++++++++++ backend/internal/entity/upstream/model.go | 127 ++++++++ .../{hosttemplate => upstream}/structs.go | 2 +- .../internal/entity/upstreamserver/filters.go | 25 ++ .../internal/entity/upstreamserver/methods.go | 154 ++++++++++ .../internal/entity/upstreamserver/model.go | 76 +++++ .../internal/entity/upstreamserver/structs.go | 15 + backend/internal/entity/user/capabilities.go | 8 +- backend/internal/entity/user/methods.go | 2 +- backend/internal/nginx/control.go | 66 +++- backend/internal/nginx/exec.go | 7 +- backend/internal/nginx/templates.go | 9 +- backend/internal/status/status.go | 10 + backend/internal/util/strings.go | 24 ++ backend/internal/util/strings_test.go | 51 ++++ backend/internal/validator/hosts.go | 12 +- backend/internal/validator/upstreams.go | 39 +++ backend/scripts/test.sh | 4 +- docker/rootfs/etc/nginx/nginx.conf | 8 +- frontend/src/Router.tsx | 6 +- ...tHostTemplates.ts => getNginxTemplates.ts} | 8 +- frontend/src/api/npm/index.ts | 2 +- frontend/src/api/npm/models.ts | 7 +- frontend/src/api/npm/responseTypes.ts | 6 +- .../components/Navigation/NavigationMenu.tsx | 15 +- .../Permissions/PermissionSelector.tsx | 6 +- frontend/src/hooks/index.ts | 2 +- ...eHostTemplates.ts => useNginxTemplates.ts} | 18 +- frontend/src/locale/src/de.json | 8 +- frontend/src/locale/src/en.json | 22 +- frontend/src/locale/src/fa.json | 4 +- .../NginxTemplatesTable.tsx} | 12 +- .../index.tsx | 16 +- scripts/ci/test-backend | 3 +- 82 files changed, 2209 insertions(+), 294 deletions(-) create mode 100644 backend/embed/api_docs/components/NginxTemplateList.json rename backend/embed/api_docs/components/{HostTemplateObject.json => NginxTemplateObject.json} (82%) rename backend/embed/api_docs/components/{HostTemplateList.json => UpstreamList.json} (88%) create mode 100644 backend/embed/api_docs/components/UpstreamObject.json rename backend/embed/api_docs/paths/{host-templates => nginx-templates}/get.json (87%) rename backend/embed/api_docs/paths/{host-templates => nginx-templates}/post.json (69%) rename backend/embed/api_docs/paths/{host-templates/hostTemplateID => nginx-templates/templateID}/delete.json (75%) rename backend/embed/api_docs/paths/{host-templates/hostTemplateID => nginx-templates/templateID}/get.json (72%) rename backend/embed/api_docs/paths/{host-templates/hostTemplateID => nginx-templates/templateID}/put.json (68%) create mode 100644 backend/embed/api_docs/paths/upstreams/get.json create mode 100644 backend/embed/api_docs/paths/upstreams/post.json create mode 100644 backend/embed/api_docs/paths/upstreams/upstreamID/delete.json create mode 100644 backend/embed/api_docs/paths/upstreams/upstreamID/get.json rename backend/internal/api/handler/{host_templates.go => nginx_templates.go} (52%) create mode 100644 backend/internal/api/handler/upstreams.go rename backend/internal/api/schema/{create_host_template.go => create_nginx_template.go} (64%) create mode 100644 backend/internal/api/schema/create_upstream.go rename backend/internal/api/schema/{update_host_template.go => update_nginx_template.go} (90%) create mode 100644 backend/internal/entity/nginxtemplate/filters.go rename backend/internal/entity/{hosttemplate => nginxtemplate}/methods.go (98%) rename backend/internal/entity/{hosttemplate => nginxtemplate}/model.go (90%) create mode 100644 backend/internal/entity/nginxtemplate/structs.go rename backend/internal/entity/{hosttemplate => upstream}/filters.go (97%) create mode 100644 backend/internal/entity/upstream/methods.go create mode 100644 backend/internal/entity/upstream/model.go rename backend/internal/entity/{hosttemplate => upstream}/structs.go (94%) create mode 100644 backend/internal/entity/upstreamserver/filters.go create mode 100644 backend/internal/entity/upstreamserver/methods.go create mode 100644 backend/internal/entity/upstreamserver/model.go create mode 100644 backend/internal/entity/upstreamserver/structs.go create mode 100644 backend/internal/status/status.go create mode 100644 backend/internal/util/strings.go create mode 100644 backend/internal/util/strings_test.go create mode 100644 backend/internal/validator/upstreams.go rename frontend/src/api/npm/{getHostTemplates.ts => getNginxTemplates.ts} (63%) rename frontend/src/hooks/{useHostTemplates.ts => useNginxTemplates.ts} (55%) rename frontend/src/pages/{HostTemplates/HostTemplatesTable.tsx => NginxTemplates/NginxTemplatesTable.tsx} (93%) rename frontend/src/pages/{HostTemplates => NginxTemplates}/index.tsx (78%) diff --git a/backend/embed/api_docs/api.swagger.json b/backend/embed/api_docs/api.swagger.json index c22f945..90ef98b 100644 --- a/backend/embed/api_docs/api.swagger.json +++ b/backend/embed/api_docs/api.swagger.json @@ -91,6 +91,25 @@ "$ref": "file://./paths/hosts/hostID/delete.json" } }, + "/nginx-templates": { + "get": { + "$ref": "file://./paths/nginx-templates/get.json" + }, + "post": { + "$ref": "file://./paths/nginx-templates/post.json" + } + }, + "/nginx-templates/{templateID}": { + "get": { + "$ref": "file://./paths/nginx-templates/templateID/get.json" + }, + "put": { + "$ref": "file://./paths/nginx-templates/templateID/put.json" + }, + "delete": { + "$ref": "file://./paths/nginx-templates/templateID/delete.json" + } + }, "/schema": { "get": { "$ref": "file://./paths/schema/get.json" @@ -139,6 +158,22 @@ "$ref": "file://./paths/tokens/post.json" } }, + "/upstreams": { + "get": { + "$ref": "file://./paths/upstreams/get.json" + }, + "post": { + "$ref": "file://./paths/upstreams/post.json" + } + }, + "/upstreams/{upstreamID}": { + "get": { + "$ref": "file://./paths/upstreams/upstreamID/get.json" + }, + "delete": { + "$ref": "file://./paths/upstreams/upstreamID/delete.json" + } + }, "/users": { "get": { "$ref": "file://./paths/users/get.json" @@ -205,11 +240,11 @@ "HostObject": { "$ref": "file://./components/HostObject.json" }, - "HostTemplateList": { - "$ref": "file://./components/HostTemplateList.json" + "NginxTemplateList": { + "$ref": "file://./components/NginxTemplateList.json" }, - "HostTemplateObject": { - "$ref": "file://./components/HostTemplateObject.json" + "NginxTemplateObject": { + "$ref": "file://./components/NginxTemplateObject.json" }, "SettingList": { "$ref": "file://./components/SettingList.json" @@ -229,6 +264,12 @@ "TokenObject": { "$ref": "file://./components/TokenObject.json" }, + "UpstreamList": { + "$ref": "file://./components/UpstreamList.json" + }, + "UpstreamObject": { + "$ref": "file://./components/UpstreamObject.json" + }, "UserAuthObject": { "$ref": "file://./components/UserAuthObject.json" }, diff --git a/backend/embed/api_docs/components/NginxTemplateList.json b/backend/embed/api_docs/components/NginxTemplateList.json new file mode 100644 index 0000000..c0c11be --- /dev/null +++ b/backend/embed/api_docs/components/NginxTemplateList.json @@ -0,0 +1,40 @@ +{ + "type": "object", + "description": "NginxTemplateList", + "additionalProperties": false, + "required": ["total", "offset", "limit", "sort"], + "properties": { + "total": { + "type": "integer", + "description": "Total number of rows" + }, + "offset": { + "type": "integer", + "description": "Pagination Offset" + }, + "limit": { + "type": "integer", + "description": "Pagination Limit" + }, + "sort": { + "type": "array", + "description": "Sorting", + "items": { + "$ref": "#/components/schemas/SortObject" + } + }, + "filter": { + "type": "array", + "description": "Filters", + "items": { + "$ref": "#/components/schemas/FilterObject" + } + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/NginxTemplateObject" + } + } + } +} diff --git a/backend/embed/api_docs/components/HostTemplateObject.json b/backend/embed/api_docs/components/NginxTemplateObject.json similarity index 82% rename from backend/embed/api_docs/components/HostTemplateObject.json rename to backend/embed/api_docs/components/NginxTemplateObject.json index ac4937a..5668660 100644 --- a/backend/embed/api_docs/components/HostTemplateObject.json +++ b/backend/embed/api_docs/components/NginxTemplateObject.json @@ -1,6 +1,6 @@ { "type": "object", - "description": "HostTemplateObject", + "description": "NginxTemplateObject", "additionalProperties": false, "required": [ "id", @@ -8,7 +8,7 @@ "modified_on", "user_id", "name", - "host_type", + "type", "template" ], "properties": { @@ -32,9 +32,9 @@ "type": "string", "minLength": 1 }, - "host_type": { + "type": { "type": "string", - "pattern": "^proxy|redirect|dead|stream$" + "pattern": "^proxy|redirect|dead|stream|upstream$" }, "template": { "type": "string", diff --git a/backend/embed/api_docs/components/HostTemplateList.json b/backend/embed/api_docs/components/UpstreamList.json similarity index 88% rename from backend/embed/api_docs/components/HostTemplateList.json rename to backend/embed/api_docs/components/UpstreamList.json index a4ad36e..316725a 100644 --- a/backend/embed/api_docs/components/HostTemplateList.json +++ b/backend/embed/api_docs/components/UpstreamList.json @@ -1,6 +1,6 @@ { "type": "object", - "description": "HostTemplateList", + "description": "UpstreamList", "additionalProperties": false, "required": ["total", "offset", "limit", "sort"], "properties": { @@ -33,7 +33,7 @@ "items": { "type": "array", "items": { - "$ref": "#/components/schemas/HostTemplateObject" + "$ref": "#/components/schemas/UpstreamObject" } } } diff --git a/backend/embed/api_docs/components/UpstreamObject.json b/backend/embed/api_docs/components/UpstreamObject.json new file mode 100644 index 0000000..0ad044a --- /dev/null +++ b/backend/embed/api_docs/components/UpstreamObject.json @@ -0,0 +1,133 @@ +{ + "type": "object", + "description": "UpstreamObject", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "user_id", + "name", + "nginx_template_id", + "ip_hash", + "ntlm", + "keepalive", + "keepalive_requests", + "keepalive_time", + "keepalive_timeout", + "advanced_config", + "status", + "error_message", + "servers" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "user_id": { + "type": "integer", + "minimum": 1 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "nginx_template_id": { + "type": "integer", + "minimum": 1 + }, + "ip_hash": { + "type": "boolean" + }, + "ntlm": { + "type": "boolean" + }, + "keepalive": { + "type": "integer" + }, + "keepalive_requests": { + "type": "integer" + }, + "keepalive_time": { + "type": "string" + }, + "keepalive_timeout": { + "type": "string" + }, + "advanced_config": { + "type": "string" + }, + "status": { + "type": "string" + }, + "error_message": { + "type": "string" + }, + "servers": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "created_on", + "modified_on", + "upstream_id", + "server", + "weight", + "max_conns", + "max_fails", + "fail_timeout", + "backup" + ], + "properties": { + "id": { + "type": "integer", + "minimum": 1 + }, + "created_on": { + "type": "integer", + "minimum": 1 + }, + "modified_on": { + "type": "integer", + "minimum": 1 + }, + "upstream_id": { + "type": "integer", + "minimum": 1 + }, + "server": { + "type": "string", + "minLength": 2 + }, + "weight": { + "type": "integer" + }, + "max_conns": { + "type": "integer" + }, + "max_fails": { + "type": "integer" + }, + "fail_timeout": { + "type": "integer" + }, + "backup": { + "type": "boolean" + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/hosts/get.json b/backend/embed/api_docs/paths/hosts/get.json index 160980e..a8683c7 100644 --- a/backend/embed/api_docs/paths/hosts/get.json +++ b/backend/embed/api_docs/paths/hosts/get.json @@ -64,7 +64,7 @@ "modified_on": 1646279455, "user_id": 2, "type": "proxy", - "host_template_id": 1, + "nginx_template_id": 1, "listen_interface": "", "domain_names": ["jc21.com"], "upstream_id": 0, @@ -78,7 +78,6 @@ "hsts_enabled": false, "hsts_subdomains": false, "paths": "", - "upstream_options": "", "advanced_config": "", "is_disabled": false } diff --git a/backend/embed/api_docs/paths/hosts/hostID/get.json b/backend/embed/api_docs/paths/hosts/hostID/get.json index 654f7df..090fdce 100644 --- a/backend/embed/api_docs/paths/hosts/hostID/get.json +++ b/backend/embed/api_docs/paths/hosts/hostID/get.json @@ -37,7 +37,7 @@ "modified_on": 1646279455, "user_id": 2, "type": "proxy", - "host_template_id": 1, + "nginx_template_id": 1, "listen_interface": "", "domain_names": ["jc21.com"], "upstream_id": 0, @@ -51,7 +51,6 @@ "hsts_enabled": false, "hsts_subdomains": false, "paths": "", - "upstream_options": "", "advanced_config": "", "is_disabled": false } diff --git a/backend/embed/api_docs/paths/hosts/hostID/put.json b/backend/embed/api_docs/paths/hosts/hostID/put.json index ec9675c..f1d38f9 100644 --- a/backend/embed/api_docs/paths/hosts/hostID/put.json +++ b/backend/embed/api_docs/paths/hosts/hostID/put.json @@ -46,7 +46,7 @@ "modified_on": 1646279455, "user_id": 2, "type": "proxy", - "host_template_id": 1, + "nginx_template_id": 1, "listen_interface": "", "domain_names": ["jc21.com"], "upstream_id": 0, @@ -60,7 +60,6 @@ "hsts_enabled": false, "hsts_subdomains": false, "paths": "", - "upstream_options": "", "advanced_config": "", "is_disabled": false } diff --git a/backend/embed/api_docs/paths/hosts/post.json b/backend/embed/api_docs/paths/hosts/post.json index e0362c6..da5dcd9 100644 --- a/backend/embed/api_docs/paths/hosts/post.json +++ b/backend/embed/api_docs/paths/hosts/post.json @@ -33,7 +33,7 @@ "modified_on": 1645700556, "user_id": 2, "type": "proxy", - "host_template_id": 1, + "nginx_template_id": 1, "listen_interface": "", "domain_names": ["jc21.com"], "upstream_id": 0, @@ -47,7 +47,6 @@ "hsts_enabled": false, "hsts_subdomains": false, "paths": "", - "upstream_options": "", "advanced_config": "", "is_disabled": false } diff --git a/backend/embed/api_docs/paths/host-templates/get.json b/backend/embed/api_docs/paths/nginx-templates/get.json similarity index 87% rename from backend/embed/api_docs/paths/host-templates/get.json rename to backend/embed/api_docs/paths/nginx-templates/get.json index 2c3ca70..c242024 100644 --- a/backend/embed/api_docs/paths/host-templates/get.json +++ b/backend/embed/api_docs/paths/nginx-templates/get.json @@ -1,7 +1,7 @@ { - "operationId": "getHostTemplates", - "summary": "Get a list of Host Templates", - "tags": ["Hosts"], + "operationId": "getNginxTemplates", + "summary": "Get a list of Nginx Templates", + "tags": ["Nginx Templates"], "parameters": [ { "in": "query", @@ -40,7 +40,7 @@ "required": ["result"], "properties": { "result": { - "$ref": "#/components/schemas/HostTemplateList" + "$ref": "#/components/schemas/NginxTemplateList" } } }, @@ -64,7 +64,7 @@ "modified_on": 1646218093, "user_id": 1, "name": "Default Proxy Template", - "host_type": "proxy", + "type": "proxy", "template": "# this is a proxy template" } ] diff --git a/backend/embed/api_docs/paths/host-templates/post.json b/backend/embed/api_docs/paths/nginx-templates/post.json similarity index 69% rename from backend/embed/api_docs/paths/host-templates/post.json rename to backend/embed/api_docs/paths/nginx-templates/post.json index dd834c8..470ad39 100644 --- a/backend/embed/api_docs/paths/host-templates/post.json +++ b/backend/embed/api_docs/paths/nginx-templates/post.json @@ -1,13 +1,13 @@ { - "operationId": "createHost", - "summary": "Create a new Host", - "tags": ["Hosts"], + "operationId": "createNginxTemplate", + "summary": "Create a new Nginx Template", + "tags": ["Nginx Templates"], "requestBody": { - "description": "Host to Create", + "description": "Template to Create", "required": true, "content": { "application/json": { - "schema": "{{schema.CreateHostTemplate}}" + "schema": "{{schema.CreateNginxTemplate}}" } } }, @@ -20,7 +20,7 @@ "required": ["result"], "properties": { "result": { - "$ref": "#/components/schemas/HostTemplateObject" + "$ref": "#/components/schemas/NginxTemplateObject" } } }, @@ -33,7 +33,7 @@ "modified_on": 1646218093, "user_id": 1, "name": "My proxy template", - "host_type": "proxy", + "type": "proxy", "template": "# this is a proxy template" } } diff --git a/backend/embed/api_docs/paths/host-templates/hostTemplateID/delete.json b/backend/embed/api_docs/paths/nginx-templates/templateID/delete.json similarity index 75% rename from backend/embed/api_docs/paths/host-templates/hostTemplateID/delete.json rename to backend/embed/api_docs/paths/nginx-templates/templateID/delete.json index f14fe9d..81e7ff5 100644 --- a/backend/embed/api_docs/paths/host-templates/hostTemplateID/delete.json +++ b/backend/embed/api_docs/paths/nginx-templates/templateID/delete.json @@ -1,17 +1,17 @@ { - "operationId": "deleteHostTemplate", - "summary": "Delete a Host Template", - "tags": ["Host Templates"], + "operationId": "deleteNginxTemplate", + "summary": "Delete a Nginx Template", + "tags": ["Nginx Templates"], "parameters": [ { "in": "path", - "name": "hostTemplateID", + "name": "templateID", "schema": { "type": "integer", "minimum": 1 }, "required": true, - "description": "Numeric ID of the Host Template", + "description": "Numeric ID of the Template", "example": 1 } ], @@ -46,7 +46,7 @@ "result": null, "error": { "code": 400, - "message": "You cannot delete a host template that is in use!" + "message": "You cannot delete a template that is in use!" } } } diff --git a/backend/embed/api_docs/paths/host-templates/hostTemplateID/get.json b/backend/embed/api_docs/paths/nginx-templates/templateID/get.json similarity index 72% rename from backend/embed/api_docs/paths/host-templates/hostTemplateID/get.json rename to backend/embed/api_docs/paths/nginx-templates/templateID/get.json index 6f16a95..01808aa 100644 --- a/backend/embed/api_docs/paths/host-templates/hostTemplateID/get.json +++ b/backend/embed/api_docs/paths/nginx-templates/templateID/get.json @@ -1,11 +1,11 @@ { - "operationId": "getHostTemplate", - "summary": "Get a Host Template object by ID", - "tags": ["Hosts"], + "operationId": "getNginxTemplate", + "summary": "Get a Nginx Template object by ID", + "tags": ["Nginx Templates"], "parameters": [ { "in": "path", - "name": "hostTemplateID", + "name": "templateID", "schema": { "type": "integer", "minimum": 1 @@ -24,7 +24,7 @@ "required": ["result"], "properties": { "result": { - "$ref": "#/components/schemas/HostTemplateObject" + "$ref": "#/components/schemas/NginxTemplateObject" } } }, @@ -36,8 +36,8 @@ "created_on": 1646218093, "modified_on": 1646218093, "user_id": 1, - "name": "Default Host Template", - "host_type": "proxy", + "name": "Default Proxy Template", + "type": "proxy", "template": "# this is a proxy template" } } diff --git a/backend/embed/api_docs/paths/host-templates/hostTemplateID/put.json b/backend/embed/api_docs/paths/nginx-templates/templateID/put.json similarity index 68% rename from backend/embed/api_docs/paths/host-templates/hostTemplateID/put.json rename to backend/embed/api_docs/paths/nginx-templates/templateID/put.json index 2993e5a..31d6647 100644 --- a/backend/embed/api_docs/paths/host-templates/hostTemplateID/put.json +++ b/backend/embed/api_docs/paths/nginx-templates/templateID/put.json @@ -1,26 +1,26 @@ { - "operationId": "updateHostTemplate", - "summary": "Update an existing Host Template", - "tags": ["Hosts"], + "operationId": "updateNginxTemplate", + "summary": "Update an existing Nginx Template", + "tags": ["Nginx Templates"], "parameters": [ { "in": "path", - "name": "hostTemplateID", + "name": "templateID", "schema": { "type": "integer", "minimum": 1 }, "required": true, - "description": "ID of the Host Template", + "description": "ID of the Template", "example": 1 } ], "requestBody": { - "description": "Host Template details to update", + "description": "Template details to update", "required": true, "content": { "application/json": { - "schema": "{{schema.UpdateHostTemplate}}" + "schema": "{{schema.UpdateNginxTemplate}}" } } }, @@ -33,7 +33,7 @@ "required": ["result"], "properties": { "result": { - "$ref": "#/components/schemas/HostTemplateObject" + "$ref": "#/components/schemas/NginxTemplateObject" } } }, @@ -46,7 +46,7 @@ "modified_on": 1646218093, "user_id": 1, "name": "My renamed proxy template", - "host_type": "proxy", + "type": "proxy", "template": "# this is a proxy template" } } diff --git a/backend/embed/api_docs/paths/streams/post.json b/backend/embed/api_docs/paths/streams/post.json index d194983..d660eed 100644 --- a/backend/embed/api_docs/paths/streams/post.json +++ b/backend/embed/api_docs/paths/streams/post.json @@ -1,11 +1,9 @@ { "operationId": "createStream", "summary": "Create a new Stream", - "tags": [ - "Streams" - ], + "tags": ["Streams"], "requestBody": { - "description": "Host to Create", + "description": "Stream to Create", "required": true, "content": { "application/json": { @@ -19,9 +17,7 @@ "content": { "application/json": { "schema": { - "required": [ - "result" - ], + "required": ["result"], "properties": { "result": { "$ref": "#/components/schemas/StreamObject" @@ -39,4 +35,4 @@ } } } -} \ No newline at end of file +} diff --git a/backend/embed/api_docs/paths/upstreams/get.json b/backend/embed/api_docs/paths/upstreams/get.json new file mode 100644 index 0000000..e52d7a9 --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/get.json @@ -0,0 +1,285 @@ +{ + "operationId": "getUpstreams", + "summary": "Get a list of Upstreams", + "tags": ["Upstreams"], + "parameters": [ + { + "in": "query", + "name": "offset", + "schema": { + "type": "number" + }, + "description": "The pagination row offset, default 0", + "example": 0 + }, + { + "in": "query", + "name": "limit", + "schema": { + "type": "number" + }, + "description": "The pagination row limit, default 10", + "example": 10 + }, + { + "in": "query", + "name": "sort", + "schema": { + "type": "string" + }, + "description": "The sorting of the list", + "example": "id,name.asc,value.desc" + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UpstreamList" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "total": 5, + "offset": 0, + "limit": 10, + "sort": [ + { + "field": "name", + "direction": "ASC" + } + ], + "items": [ + { + "id": 1, + "created_on": 1672804124, + "modified_on": 1672804124, + "user_id": 2, + "name": "API servers", + "nginx_template_id": 5, + "ip_hash": true, + "ntlm": false, + "keepalive": 10, + "keepalive_requests": 10, + "keepalive_time": "60s", + "keepalive_timeout": "3s", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 1, + "created_on": 1672804124, + "modified_on": 1672804124, + "upstream_group_id": 1, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 2, + "created_on": 1672804124, + "modified_on": 1672804124, + "upstream_group_id": 1, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + }, + { + "id": 2, + "created_on": 1672804197, + "modified_on": 1672804197, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 3, + "created_on": 1672804197, + "modified_on": 1672804197, + "upstream_group_id": 2, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 4, + "created_on": 1672804197, + "modified_on": 1672804197, + "upstream_group_id": 2, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + }, + { + "id": 3, + "created_on": 1672804200, + "modified_on": 1672804200, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 5, + "created_on": 1672804200, + "modified_on": 1672804200, + "upstream_group_id": 3, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 6, + "created_on": 1672804200, + "modified_on": 1672804200, + "upstream_group_id": 3, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + }, + { + "id": 4, + "created_on": 1672804201, + "modified_on": 1672804201, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 7, + "created_on": 1672804201, + "modified_on": 1672804201, + "upstream_group_id": 4, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 8, + "created_on": 1672804201, + "modified_on": 1672804201, + "upstream_group_id": 4, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + }, + { + "id": 5, + "created_on": 1672804201, + "modified_on": 1672804201, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ok", + "error_message": "", + "servers": [ + { + "id": 9, + "created_on": 1672804201, + "modified_on": 1672804201, + "upstream_group_id": 5, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 10, + "created_on": 1672804201, + "modified_on": 1672804201, + "upstream_group_id": 5, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/upstreams/post.json b/backend/embed/api_docs/paths/upstreams/post.json new file mode 100644 index 0000000..8bab6d3 --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/post.json @@ -0,0 +1,80 @@ +{ + "operationId": "createUpstream", + "summary": "Create a new Upstream", + "tags": ["Upstreams"], + "requestBody": { + "description": "Upstream to Create", + "required": true, + "content": { + "application/json": { + "schema": "{{schema.CreateUpstream}}" + } + } + }, + "responses": { + "201": { + "description": "201 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UpstreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 6, + "created_on": 1672806857, + "modified_on": 1672806857, + "user_id": 2, + "name": "API servers 2", + "nginx_template_id": 5, + "ip_hash": false, + "ntlm": false, + "keepalive": 0, + "keepalive_requests": 0, + "keepalive_time": "", + "keepalive_timeout": "", + "advanced_config": "", + "status": "ready", + "error_message": "", + "servers": [ + { + "id": 11, + "created_on": 1672806857, + "modified_on": 1672806857, + "upstream_id": 6, + "server": "192.168.0.10:80", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 12, + "created_on": 1672806857, + "modified_on": 1672806857, + "upstream_id": 6, + "server": "192.168.0.11:80", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/upstreams/upstreamID/delete.json b/backend/embed/api_docs/paths/upstreams/upstreamID/delete.json new file mode 100644 index 0000000..0c54708 --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/upstreamID/delete.json @@ -0,0 +1,58 @@ +{ + "operationId": "deleteUpstream", + "summary": "Delete a Upstream", + "tags": ["Upstreams"], + "parameters": [ + { + "in": "path", + "name": "upstreamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "Numeric ID of the Upstream", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": true + } + } + } + } + } + }, + "400": { + "description": "400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeletedItemResponse" + }, + "examples": { + "default": { + "value": { + "result": null, + "error": { + "code": 400, + "message": "You cannot delete a Upstream that is in use!" + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/api_docs/paths/upstreams/upstreamID/get.json b/backend/embed/api_docs/paths/upstreams/upstreamID/get.json new file mode 100644 index 0000000..8303b2c --- /dev/null +++ b/backend/embed/api_docs/paths/upstreams/upstreamID/get.json @@ -0,0 +1,81 @@ +{ + "operationId": "getUpstream", + "summary": "Get a Upstream object by ID", + "tags": ["Upstreams"], + "parameters": [ + { + "in": "path", + "name": "upstreamID", + "schema": { + "type": "integer", + "minimum": 1 + }, + "required": true, + "description": "ID of the Upstream", + "example": 1 + } + ], + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "required": ["result"], + "properties": { + "result": { + "$ref": "#/components/schemas/UpstreamObject" + } + } + }, + "examples": { + "default": { + "value": { + "result": { + "id": 1, + "created_on": 1672786008, + "modified_on": 1672786008, + "user_id": 2, + "name": "API servers 3", + "ip_hash": true, + "ntlm": false, + "keepalive": 10, + "keepalive_requests": 10, + "keepalive_time": "60s", + "keepalive_timeout": "3s", + "advanced_config": "", + "servers": [ + { + "id": 1, + "created_on": 1672786009, + "modified_on": 1672786009, + "upstream_id": 1, + "server": "api1.localhost:1234", + "weight": 100, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": false + }, + { + "id": 2, + "created_on": 1672786009, + "modified_on": 1672786009, + "upstream_id": 1, + "server": "api2.localhost:1234", + "weight": 50, + "max_conns": 0, + "max_fails": 0, + "fail_timeout": 0, + "backup": true + } + ] + } + } + } + } + } + } + } + } +} diff --git a/backend/embed/migrations/20201013035318_initial_schema.sql b/backend/embed/migrations/20201013035318_initial_schema.sql index 3b95c50..4d5cb1e 100644 --- a/backend/embed/migrations/20201013035318_initial_schema.sql +++ b/backend/embed/migrations/20201013035318_initial_schema.sql @@ -123,7 +123,6 @@ CREATE TABLE IF NOT EXISTS `stream` user_id INTEGER NOT NULL, listen_interface TEXT NOT NULL, incoming_port INTEGER NOT NULL, - upstream_options TEXT NOT NULL, tcp_forwarding INTEGER NOT NULL DEFAULT 0, udp_forwarding INTEGER NOT NULL DEFAULT 0, advanced_config TEXT NOT NULL, @@ -138,13 +137,36 @@ CREATE TABLE IF NOT EXISTS `upstream` created_on INTEGER NOT NULL DEFAULT 0, modified_on INTEGER NOT NULL DEFAULT 0, user_id INTEGER NOT NULL, - hosts TEXT NOT NULL, - balance_method TEXT NOT NULL, - max_fails INTEGER NOT NULL DEFAULT 1, - fail_timeout INTEGER NOT NULL DEFAULT 10, + name TEXT NOT NULL, + nginx_template_id INTEGER NOT NULL, + ip_hash INTEGER NOT NULL DEFAULT 0, + ntlm INTEGER NOT NULL DEFAULT 0, + keepalive INTEGER NOT NULL DEFAULT 0, + keepalive_requests INTEGER NOT NULL DEFAULT 0, + keepalive_time TEXT NOT NULL DEFAULT "", + keepalive_timeout TEXT NOT NULL DEFAULT "", advanced_config TEXT NOT NULL, + status TEXT NOT NULL DEFAULT "", + error_message TEXT NOT NULL DEFAULT "", is_deleted INTEGER NOT NULL DEFAULT 0, - FOREIGN KEY (user_id) REFERENCES user (id) + FOREIGN KEY (user_id) REFERENCES user (id), + FOREIGN KEY (nginx_template_id) REFERENCES nginx_template (id) +); + +CREATE TABLE IF NOT EXISTS `upstream_server` +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_on INTEGER NOT NULL DEFAULT 0, + modified_on INTEGER NOT NULL DEFAULT 0, + upstream_id INTEGER NOT NULL, + server TEXT NOT NULL, + weight INTEGER NOT NULL DEFAULT 0, + max_conns INTEGER NOT NULL DEFAULT 0, + max_fails INTEGER NOT NULL DEFAULT 0, + fail_timeout INTEGER NOT NULL DEFAULT 0, + backup INTEGER NOT NULL DEFAULT 0, + is_deleted INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY (upstream_id) REFERENCES upstream (id) ); CREATE TABLE IF NOT EXISTS `access_list` @@ -159,14 +181,14 @@ CREATE TABLE IF NOT EXISTS `access_list` FOREIGN KEY (user_id) REFERENCES user (id) ); -CREATE TABLE IF NOT EXISTS `host_template` +CREATE TABLE IF NOT EXISTS `nginx_template` ( id INTEGER PRIMARY KEY AUTOINCREMENT, created_on INTEGER NOT NULL DEFAULT 0, modified_on INTEGER NOT NULL DEFAULT 0, user_id INTEGER NOT NULL, name TEXT NOT NULL, - host_type TEXT NOT NULL, + type TEXT NOT NULL, template TEXT NOT NULL, is_deleted INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES user (id) @@ -179,7 +201,7 @@ CREATE TABLE IF NOT EXISTS `host` modified_on INTEGER NOT NULL DEFAULT 0, user_id INTEGER NOT NULL, type TEXT NOT NULL, - host_template_id INTEGER NOT NULL, + nginx_template_id INTEGER NOT NULL, listen_interface TEXT NOT NULL DEFAULT "", domain_names TEXT NOT NULL, upstream_id INTEGER NOT NULL DEFAULT 0, @@ -193,14 +215,13 @@ CREATE TABLE IF NOT EXISTS `host` hsts_enabled INTEGER NOT NULL DEFAULT 0, hsts_subdomains INTEGER NOT NULL DEFAULT 0, paths TEXT NOT NULL DEFAULT "", - upstream_options TEXT NOT NULL DEFAULT "", advanced_config TEXT NOT NULL DEFAULT "", status TEXT NOT NULL DEFAULT "", error_message TEXT NOT NULL DEFAULT "", is_disabled INTEGER NOT NULL DEFAULT 0, is_deleted INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (user_id) REFERENCES user (id), - FOREIGN KEY (host_template_id) REFERENCES host_template (id), + FOREIGN KEY (nginx_template_id) REFERENCES nginx_template (id), FOREIGN KEY (upstream_id) REFERENCES upstream (id), FOREIGN KEY (certificate_id) REFERENCES certificate (id), FOREIGN KEY (access_list_id) REFERENCES access_list (id) diff --git a/backend/embed/migrations/20201013035839_initial_data.sql b/backend/embed/migrations/20201013035839_initial_data.sql index fa71871..d7a9112 100644 --- a/backend/embed/migrations/20201013035839_initial_data.sql +++ b/backend/embed/migrations/20201013035839_initial_data.sql @@ -16,8 +16,8 @@ INSERT INTO `capability` ( ("dns-providers.manage"), ("hosts.view"), ("hosts.manage"), - ("host-templates.view"), - ("host-templates.manage"), + ("nginx-templates.view"), + ("nginx-templates.manage"), ("settings.manage"), ("streams.view"), ("streams.manage"), @@ -131,12 +131,12 @@ INSERT INTO `user` ( ); -- Host Templates -INSERT INTO `host_template` ( +INSERT INTO `nginx_template` ( created_on, modified_on, user_id, name, - host_type, + type, template ) VALUES ( strftime('%s', 'now'), @@ -144,7 +144,119 @@ INSERT INTO `host_template` ( (SELECT id FROM user WHERE is_system = 1 LIMIT 1), "Default Proxy Template", "proxy", - "# this is a proxy template" + "# ------------------------------------------------------------ +{{#each Host.DomainNames}} +# {{this}} +{{/each}} +# ------------------------------------------------------------ + +{{#if Host.IsDisabled}} +# This Proxy Host is disabled and will not generate functional config +{{/if}} + +{{#unless Host.IsDisabled}} +server { + set $forward_scheme {{Host.ForwardScheme}}; + set $server ""{{Host.ForwardHost}}""; + set $port {{Host.ForwardPort}}; + + {{#if Config.Ipv4}} + listen 80; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:80; + {{/if}} + + {{#if Certificate.ID}} + listen 443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + {{#if Config.Ipv6}} + listen [::]:443 ssl {{#if Host.HTTP2Support}}http2{{/if}}; + {{/if}} + + server_name {{#each Host.DomainNames}}{{this}} {{/each}}; + + {{#if Certificate.ID}} + include conf.d/include/ssl-ciphers.conf; + {{#if Certificate.IsAcme}} + ssl_certificate {{Certificate.Folder}}/fullchain.pem; + ssl_certificate_key {{Certificate.Folder}}/privkey.pem; + {{else}} + # Custom SSL + ssl_certificate /data/custom_ssl/npm-{{Certicicate.ID}}/fullchain.pem; + ssl_certificate_key /data/custom_ssl/npm-{{Certificate.ID}}/privkey.pem; + {{/if}} + {{/if}} + + {{#if Host.CachingEnabled}} + include conf.d/include/assets.conf; + {{/if}} + + {{#if Host.BlockExploits}} + include conf.d/include/block-exploits.conf; + {{/if}} + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security ""max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload"" always; + {{/if}} + # Force SSL + include conf.d/include/force-ssl.conf; + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + access_log /data/logs/host-{{Host.ID}}_access.log proxy; + error_log /data/logs/host-{{Host.ID}}_error.log warn; + + {{Host.AdvancedConfig}} + + # locations ? + + # default location: + location / { + {{#if Host.AccessListID}} + # Authorization + auth_basic ""Authorization required""; + auth_basic_user_file /data/access/{{Host.AccessListID}}; + # access_list.passauth ? todo + {{/if}} + + # Access Rules ? todo + + # Access checks must...? todo + + {{#if Certificate.ID}} + {{#if Host.SSLForced}} + {{#if Host.HSTSEnabled}} + # HSTS (ngx_http_headers_module is required) (63072000 seconds = 2 years) + add_header Strict-Transport-Security ""max-age=63072000;{{#if Host.HSTSSubdomains}} includeSubDomains;{{/if}} preload"" always; + {{/if}} + {{/if}} + {{/if}} + + {{#if Host.AllowWebsocketUpgrade}} + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $http_connection; + proxy_http_version 1.1; + {{/if}} + + # Proxy! + include conf.d/include/proxy.conf; + } + + # Legacy Custom Configuration + include /data/nginx/custom/server_proxy[.]conf; +} +{{/unless}} +" ), ( strftime('%s', 'now'), strftime('%s', 'now'), @@ -166,6 +278,55 @@ INSERT INTO `host_template` ( "Default Stream Template", "stream", "# this is a stream template" +), ( + strftime('%s', 'now'), + strftime('%s', 'now'), + (SELECT id FROM user WHERE is_system = 1 LIMIT 1), + "Default Upstream Template", + "upstream", + "# ------------------------------------------------------------ +# Upstream {{Upstream.ID}}: {{Upstream.Name}} +# ------------------------------------------------------------ + +{{#unless Upstream.IsDeleted~}} + +upstream npm_upstream_{{Upstream.ID}} { + + {{#if Upstream.IPHash~}} + ip_hash; + {{~/if}} + + {{#if Upstream.NTLM~}} + ntlm; + {{~/if}} + + {{#if Upstream.Keepalive~}} + keepalive {{Upstream.Keepalive}}; + {{~/if}} + + {{#if Upstream.KeepaliveRequests~}} + keepalive_requests {{Upstream.KeepaliveRequests}}; + {{~/if}} + + {{#if Upstream.KeepaliveTime~}} + keepalive_time {{Upstream.KeepaliveTime}}; + {{~/if}} + + {{#if Upstream.KeepaliveTimeout~}} + keepalive_timeout {{Upstream.KeepaliveTimeout}}; + {{~/if}} + + {{Upstream.AdvancedConfig}} + + {{#each Upstream.Servers~}} + {{#unless IsDeleted~}} + server {{Server}} {{#if Weight}}weight={{Weight}} {{/if}}{{#if MaxConns}}max_conns={{MaxConns}} {{/if}}{{#if MaxFails}}max_fails={{MaxFails}} {{/if}}{{#if FailTimeout}}fail_timeout={{FailTimeout}} {{/if}}{{#if Backup}}backup{{/if}}; + {{/unless}} + {{/each}} +} + +{{~/unless~}} +" ); -- migrate:down diff --git a/backend/internal/acme/acmesh.go b/backend/internal/acme/acmesh.go index 462eba3..7d95b1d 100644 --- a/backend/internal/acme/acmesh.go +++ b/backend/internal/acme/acmesh.go @@ -104,7 +104,7 @@ func shExec(args []string, envs []string) (string, error) { c := exec.Command(acmeSh, args...) c.Env = append(getCommonEnvVars(), envs...) - b, e := c.Output() + b, e := c.CombinedOutput() if e != nil { logger.Error("AcmeShError", fmt.Errorf("Command error: %s -- %v\n%+v", acmeSh, args, e)) diff --git a/backend/internal/acme/acmesh_test.go b/backend/internal/acme/acmesh_test.go index 837aa36..08969b8 100644 --- a/backend/internal/acme/acmesh_test.go +++ b/backend/internal/acme/acmesh_test.go @@ -11,15 +11,6 @@ import ( "github.com/stretchr/testify/assert" ) -// Tear up/down -/* -func TestMain(m *testing.M) { - config.Init(&version, &commit, &sentryDSN) - code := m.Run() - os.Exit(code) -} -*/ - // TODO configurable const acmeLogFile = "/data/logs/acme.sh.log" diff --git a/backend/internal/api/handler/hosts.go b/backend/internal/api/handler/hosts.go index e4c6e11..3a77f10 100644 --- a/backend/internal/api/handler/hosts.go +++ b/backend/internal/api/handler/hosts.go @@ -1,6 +1,7 @@ package handler import ( + "database/sql" "encoding/json" "fmt" "net/http" @@ -45,13 +46,16 @@ func GetHost() func(http.ResponseWriter, *http.Request) { return } - hostObject, err := host.GetByID(hostID) - if err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } else { + item, err := host.GetByID(hostID) + switch err { + case sql.ErrNoRows: + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + case nil: // nolint: errcheck,gosec - hostObject.Expand(getExpandFromContext(r)) - h.ResultResponseJSON(w, r, http.StatusOK, hostObject) + item.Expand(getExpandFromContext(r)) + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) } } } @@ -111,6 +115,11 @@ func UpdateHost() func(http.ResponseWriter, *http.Request) { return } + if err = validator.ValidateHost(hostObject); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + if err = hostObject.Save(false); err != nil { h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) return @@ -137,11 +146,14 @@ func DeleteHost() func(http.ResponseWriter, *http.Request) { return } - host, err := host.GetByID(hostID) - if err != nil { + item, err := host.GetByID(hostID) + switch err { + case sql.ErrNoRows: + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + default: h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } else { - h.ResultResponseJSON(w, r, http.StatusOK, host.Delete()) } } } diff --git a/backend/internal/api/handler/host_templates.go b/backend/internal/api/handler/nginx_templates.go similarity index 52% rename from backend/internal/api/handler/host_templates.go rename to backend/internal/api/handler/nginx_templates.go index 3ce1ca3..a459691 100644 --- a/backend/internal/api/handler/host_templates.go +++ b/backend/internal/api/handler/nginx_templates.go @@ -1,6 +1,7 @@ package handler import ( + "database/sql" "encoding/json" "fmt" "net/http" @@ -8,13 +9,12 @@ import ( c "npm/internal/api/context" h "npm/internal/api/http" "npm/internal/api/middleware" - "npm/internal/entity/host" - "npm/internal/entity/hosttemplate" + "npm/internal/entity/nginxtemplate" ) -// GetHostTemplates will return a list of Host Templates -// Route: GET /host-templates -func GetHostTemplates() func(http.ResponseWriter, *http.Request) { +// GetNginxTemplates will return a list of Nginx Templates +// Route: GET /nginx-templates +func GetNginxTemplates() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { pageInfo, err := getPageInfoFromRequest(r) if err != nil { @@ -22,18 +22,18 @@ func GetHostTemplates() func(http.ResponseWriter, *http.Request) { return } - hosts, err := hosttemplate.List(pageInfo, middleware.GetFiltersFromContext(r)) + items, err := nginxtemplate.List(pageInfo, middleware.GetFiltersFromContext(r)) if err != nil { h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) } else { - h.ResultResponseJSON(w, r, http.StatusOK, hosts) + h.ResultResponseJSON(w, r, http.StatusOK, items) } } } -// GetHostTemplate will return a single Host Template -// Route: GET /host-templates/{templateID} -func GetHostTemplate() func(http.ResponseWriter, *http.Request) { +// GetNginxTemplate will return a single Nginx Template +// Route: GET /nginx-templates/{templateID} +func GetNginxTemplate() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var err error var templateID int @@ -42,23 +42,26 @@ func GetHostTemplate() func(http.ResponseWriter, *http.Request) { return } - host, err := hosttemplate.GetByID(templateID) - if err != nil { + item, err := nginxtemplate.GetByID(templateID) + switch err { + case sql.ErrNoRows: + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } else { - h.ResultResponseJSON(w, r, http.StatusOK, host) } } } -// CreateHostTemplate will create a Host Template -// Route: POST /host-templates -func CreateHostTemplate() func(http.ResponseWriter, *http.Request) { +// CreateNginxTemplate will create a Nginx Template +// Route: POST /nginx-templates +func CreateNginxTemplate() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) - var newHostTemplate hosttemplate.Model - err := json.Unmarshal(bodyBytes, &newHostTemplate) + var newNginxTemplate nginxtemplate.Model + err := json.Unmarshal(bodyBytes, &newNginxTemplate) if err != nil { h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) return @@ -66,20 +69,20 @@ func CreateHostTemplate() func(http.ResponseWriter, *http.Request) { // Get userID from token userID, _ := r.Context().Value(c.UserIDCtxKey).(int) - newHostTemplate.UserID = userID + newNginxTemplate.UserID = userID - if err = newHostTemplate.Save(); err != nil { - h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host Template: %s", err.Error()), nil) + if err = newNginxTemplate.Save(); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Nginx Template: %s", err.Error()), nil) return } - h.ResultResponseJSON(w, r, http.StatusOK, newHostTemplate) + h.ResultResponseJSON(w, r, http.StatusOK, newNginxTemplate) } } -// UpdateHostTemplate updates a host template -// Route: PUT /host-templates/{templateID} -func UpdateHostTemplate() func(http.ResponseWriter, *http.Request) { +// UpdateNginxTemplate updates a nginx template +// Route: PUT /nginx-templates/{templateID} +func UpdateNginxTemplate() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var err error var templateID int @@ -90,30 +93,30 @@ func UpdateHostTemplate() func(http.ResponseWriter, *http.Request) { // reconfigure, _ := getQueryVarBool(r, "reconfigure", false, false) - hostTemplate, err := hosttemplate.GetByID(templateID) + nginxTemplate, err := nginxtemplate.GetByID(templateID) if err != nil { h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) } else { bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) - err := json.Unmarshal(bodyBytes, &hostTemplate) + err := json.Unmarshal(bodyBytes, &nginxTemplate) if err != nil { h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) return } - if err = hostTemplate.Save(); err != nil { + if err = nginxTemplate.Save(); err != nil { h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) return } - h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate) + h.ResultResponseJSON(w, r, http.StatusOK, nginxTemplate) } } } -// DeleteHostTemplate removes a host template -// Route: DELETE /host-templates/{templateID} -func DeleteHostTemplate() func(http.ResponseWriter, *http.Request) { +// DeleteNginxTemplate removes a nginx template +// Route: DELETE /nginx-templates/{templateID} +func DeleteNginxTemplate() func(http.ResponseWriter, *http.Request) { return func(w http.ResponseWriter, r *http.Request) { var err error var templateID int @@ -122,11 +125,14 @@ func DeleteHostTemplate() func(http.ResponseWriter, *http.Request) { return } - hostTemplate, err := host.GetByID(templateID) - if err != nil { + item, err := nginxtemplate.GetByID(templateID) + switch err { + case sql.ErrNoRows: + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + default: h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) - } else { - h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate.Delete()) } } } diff --git a/backend/internal/api/handler/schema.go b/backend/internal/api/handler/schema.go index 2fb37b5..b0fdad7 100644 --- a/backend/internal/api/handler/schema.go +++ b/backend/internal/api/handler/schema.go @@ -95,8 +95,8 @@ func replaceIncomingSchemas(swaggerSchema []byte) []byte { str = strings.ReplaceAll(str, `"{{schema.CreateHost}}"`, schema.CreateHost()) str = strings.ReplaceAll(str, `"{{schema.UpdateHost}}"`, schema.UpdateHost()) - str = strings.ReplaceAll(str, `"{{schema.CreateHostTemplate}}"`, schema.CreateHostTemplate()) - str = strings.ReplaceAll(str, `"{{schema.UpdateHostTemplate}}"`, schema.UpdateHostTemplate()) + str = strings.ReplaceAll(str, `"{{schema.CreateNginxTemplate}}"`, schema.CreateNginxTemplate()) + str = strings.ReplaceAll(str, `"{{schema.UpdateNginxTemplate}}"`, schema.UpdateNginxTemplate()) str = strings.ReplaceAll(str, `"{{schema.CreateStream}}"`, schema.CreateStream()) str = strings.ReplaceAll(str, `"{{schema.UpdateStream}}"`, schema.UpdateStream()) @@ -104,5 +104,7 @@ func replaceIncomingSchemas(swaggerSchema []byte) []byte { str = strings.ReplaceAll(str, `"{{schema.CreateDNSProvider}}"`, schema.CreateDNSProvider()) str = strings.ReplaceAll(str, `"{{schema.UpdateDNSProvider}}"`, schema.UpdateDNSProvider()) + str = strings.ReplaceAll(str, `"{{schema.CreateUpstream}}"`, schema.CreateUpstream()) + return []byte(str) } diff --git a/backend/internal/api/handler/upstreams.go b/backend/internal/api/handler/upstreams.go new file mode 100644 index 0000000..e505d1e --- /dev/null +++ b/backend/internal/api/handler/upstreams.go @@ -0,0 +1,129 @@ +package handler + +import ( + "database/sql" + "encoding/json" + "fmt" + "net/http" + + c "npm/internal/api/context" + h "npm/internal/api/http" + "npm/internal/api/middleware" + "npm/internal/entity/upstream" + "npm/internal/jobqueue" + "npm/internal/logger" + "npm/internal/nginx" + "npm/internal/validator" +) + +// GetUpstreams will return a list of Upstreams +// Route: GET /upstreams +func GetUpstreams() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + pageInfo, err := getPageInfoFromRequest(r) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + items, err := upstream.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r)) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } else { + h.ResultResponseJSON(w, r, http.StatusOK, items) + } + } +} + +// GetUpstream will return a single Upstream +// Route: GET /upstreams/{upstreamID} +func GetUpstream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var upstreamID int + if upstreamID, err = getURLParamInt(r, "upstreamID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := upstream.GetByID(upstreamID) + switch err { + case sql.ErrNoRows: + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + case nil: + // nolint: errcheck,gosec + item.Expand(getExpandFromContext(r)) + h.ResultResponseJSON(w, r, http.StatusOK, item) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +// CreateUpstream will create a Upstream +// Route: POST /upstreams +func CreateUpstream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte) + + var newUpstream upstream.Model + err := json.Unmarshal(bodyBytes, &newUpstream) + if err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil) + return + } + + // Get userID from token + userID, _ := r.Context().Value(c.UserIDCtxKey).(int) + newUpstream.UserID = userID + + if err = validator.ValidateUpstream(newUpstream); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + if err = newUpstream.Save(false); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Upstream: %s", err.Error()), nil) + return + } + + configureUpstream(newUpstream) + + h.ResultResponseJSON(w, r, http.StatusOK, newUpstream) + } +} + +// DeleteUpstream removes a upstream +// Route: DELETE /upstreams/{upstreamID} +func DeleteUpstream() func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + var err error + var upstreamID int + if upstreamID, err = getURLParamInt(r, "upstreamID"); err != nil { + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + return + } + + item, err := upstream.GetByID(upstreamID) + switch err { + case sql.ErrNoRows: + h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil) + case nil: + h.ResultResponseJSON(w, r, http.StatusOK, item.Delete()) + default: + h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil) + } + } +} + +func configureUpstream(u upstream.Model) { + err := jobqueue.AddJob(jobqueue.Job{ + Name: "NginxConfigureUpstream", + Action: func() error { + return nginx.ConfigureUpstream(u) + }, + }) + if err != nil { + logger.Error("ConfigureUpstreamError", err) + } +} diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go index 27bc32b..e73c9e5 100644 --- a/backend/internal/api/router.go +++ b/backend/internal/api/router.go @@ -12,9 +12,10 @@ import ( "npm/internal/entity/certificateauthority" "npm/internal/entity/dnsprovider" "npm/internal/entity/host" - "npm/internal/entity/hosttemplate" + "npm/internal/entity/nginxtemplate" "npm/internal/entity/setting" "npm/internal/entity/stream" + "npm/internal/entity/upstream" "npm/internal/entity/user" "npm/internal/logger" @@ -169,16 +170,16 @@ func applyRoutes(r chi.Router) chi.Router { Put("/{hostID:[0-9]+}", handler.UpdateHost()) }) - // Host Templates - r.With(middleware.EnforceSetup(true)).Route("/host-templates", func(r chi.Router) { - r.With(middleware.Enforce(user.CapabilityHostTemplatesView), middleware.Filters(hosttemplate.GetFilterSchema())). - Get("/", handler.GetHostTemplates()) - r.With(middleware.Enforce(user.CapabilityHostTemplatesView)).Get("/{templateID:[0-9]+}", handler.GetHostTemplates()) - r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).Delete("/{templateID:[0-9]+}", handler.DeleteHostTemplate()) - r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.CreateHostTemplate())). - Post("/", handler.CreateHostTemplate()) - r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.UpdateHostTemplate())). - Put("/{templateID:[0-9]+}", handler.UpdateHostTemplate()) + // Nginx Templates + r.With(middleware.EnforceSetup(true)).Route("/nginx-templates", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityNginxTemplatesView), middleware.Filters(nginxtemplate.GetFilterSchema())). + Get("/", handler.GetNginxTemplates()) + r.With(middleware.Enforce(user.CapabilityNginxTemplatesView)).Get("/{templateID:[0-9]+}", handler.GetNginxTemplates()) + r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage)).Delete("/{templateID:[0-9]+}", handler.DeleteNginxTemplate()) + r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage)).With(middleware.EnforceRequestSchema(schema.CreateNginxTemplate())). + Post("/", handler.CreateNginxTemplate()) + r.With(middleware.Enforce(user.CapabilityNginxTemplatesManage)).With(middleware.EnforceRequestSchema(schema.UpdateNginxTemplate())). + Put("/{templateID:[0-9]+}", handler.UpdateNginxTemplate()) }) // Streams @@ -192,6 +193,16 @@ func applyRoutes(r chi.Router) chi.Router { r.With(middleware.Enforce(user.CapabilityStreamsManage)).With(middleware.EnforceRequestSchema(schema.UpdateStream())). Put("/{hostID:[0-9]+}", handler.UpdateStream()) }) + + // Upstreams + r.With(middleware.EnforceSetup(true)).Route("/upstreams", func(r chi.Router) { + r.With(middleware.Enforce(user.CapabilityHostsView), middleware.Filters(upstream.GetFilterSchema())). + Get("/", handler.GetUpstreams()) + r.With(middleware.Enforce(user.CapabilityHostsView)).Get("/{upstreamID:[0-9]+}", handler.GetUpstream()) + r.With(middleware.Enforce(user.CapabilityHostsManage)).Delete("/{upstreamID:[0-9]+}", handler.DeleteUpstream()) + r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.CreateUpstream())). + Post("/", handler.CreateUpstream()) + }) }) return r diff --git a/backend/internal/api/schema/create_host.go b/backend/internal/api/schema/create_host.go index 0c44842..18347bb 100644 --- a/backend/internal/api/schema/create_host.go +++ b/backend/internal/api/schema/create_host.go @@ -17,14 +17,14 @@ func CreateHost() string { "required": [ "type", "domain_names", - "host_template_id" + "nginx_template_id" ], "properties": { "type": { "type": "string", "pattern": "^proxy$" }, - "host_template_id": { + "nginx_template_id": { "type": "integer", "minimum": 1 }, @@ -63,9 +63,6 @@ func CreateHost() string { "paths": { "type": "string" }, - "upstream_options": { - "type": "string" - }, "advanced_config": { "type": "string" }, diff --git a/backend/internal/api/schema/create_host_template.go b/backend/internal/api/schema/create_nginx_template.go similarity index 64% rename from backend/internal/api/schema/create_host_template.go rename to backend/internal/api/schema/create_nginx_template.go index ebb0cf4..7fe3fc6 100644 --- a/backend/internal/api/schema/create_host_template.go +++ b/backend/internal/api/schema/create_nginx_template.go @@ -1,14 +1,14 @@ package schema -// CreateHostTemplate is the schema for incoming data validation -func CreateHostTemplate() string { +// CreateNginxTemplate is the schema for incoming data validation +func CreateNginxTemplate() string { return ` { "type": "object", "additionalProperties": false, "required": [ "name", - "host_type", + "type", "template" ], "properties": { @@ -16,9 +16,9 @@ func CreateHostTemplate() string { "type": "string", "minLength": 1 }, - "host_type": { + "type": { "type": "string", - "pattern": "^proxy|redirect|dead|stream$" + "pattern": "^proxy|redirect|dead|stream|upstream$" }, "template": { "type": "string", diff --git a/backend/internal/api/schema/create_upstream.go b/backend/internal/api/schema/create_upstream.go new file mode 100644 index 0000000..3f2be46 --- /dev/null +++ b/backend/internal/api/schema/create_upstream.go @@ -0,0 +1,73 @@ +package schema + +import "fmt" + +// CreateUpstream is the schema for incoming data validation +func CreateUpstream() string { + return fmt.Sprintf(` + { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "servers", + "nginx_template_id" + ], + "properties": { + "name": %s, + "nginx_template_id": { + "type": "integer", + "minimum": 1 + }, + "advanced_config": %s, + "ip_hash": { + "type": "boolean" + }, + "ntlm": { + "type": "boolean" + }, + "keepalive": { + "type": "integer" + }, + "keepalive_requests": { + "type": "integer" + }, + "keepalive_time": { + "type": "string" + }, + "keepalive_timeout": { + "type": "string" + }, + "servers": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "server" + ], + "properties": { + "server": %s, + "weight": { + "type": "integer" + }, + "max_conns": { + "type": "integer" + }, + "max_fails": { + "type": "integer" + }, + "fail_timeout": { + "type": "integer" + }, + "backup": { + "type": "boolean" + } + } + } + } + } + } +`, stringMinMax(1, 100), stringMinMax(0, 1024), stringMinMax(2, 255)) +} diff --git a/backend/internal/api/schema/update_host.go b/backend/internal/api/schema/update_host.go index 78d1322..21643bb 100644 --- a/backend/internal/api/schema/update_host.go +++ b/backend/internal/api/schema/update_host.go @@ -10,7 +10,7 @@ func UpdateHost() string { "additionalProperties": false, "minProperties": 1, "properties": { - "host_template_id": { + "nginx_template_id": { "type": "integer", "minimum": 1 }, diff --git a/backend/internal/api/schema/update_host_template.go b/backend/internal/api/schema/update_nginx_template.go similarity index 90% rename from backend/internal/api/schema/update_host_template.go rename to backend/internal/api/schema/update_nginx_template.go index d51cf34..4ba59cd 100644 --- a/backend/internal/api/schema/update_host_template.go +++ b/backend/internal/api/schema/update_nginx_template.go @@ -1,7 +1,7 @@ package schema // UpdateHostTemplate is the schema for incoming data validation -func UpdateHostTemplate() string { +func UpdateNginxTemplate() string { return ` { "type": "object", diff --git a/backend/internal/config/folders.go b/backend/internal/config/folders.go index bf3d43c..e82570c 100644 --- a/backend/internal/config/folders.go +++ b/backend/internal/config/folders.go @@ -19,6 +19,7 @@ func createDataFolders() { "nginx/hosts", "nginx/streams", "nginx/temp", + "nginx/upstreams", } for _, folder := range folders { diff --git a/backend/internal/entity/certificate/methods.go b/backend/internal/entity/certificate/methods.go index f08cc5c..b020469 100644 --- a/backend/internal/entity/certificate/methods.go +++ b/backend/internal/entity/certificate/methods.go @@ -127,7 +127,7 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) } // Get rows - var items []Model + items := make([]Model, 0) query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) err := db.Select(&items, query, params...) if err != nil { diff --git a/backend/internal/entity/certificate/model.go b/backend/internal/entity/certificate/model.go index d3b4f18..ac276fc 100644 --- a/backend/internal/entity/certificate/model.go +++ b/backend/internal/entity/certificate/model.go @@ -267,9 +267,14 @@ func (m *Model) Request() error { // GetTemplate will convert the Model to a Template func (m *Model) GetTemplate() Template { + if m.ID == 0 { + // No or empty certificate object, happens when the host has no cert + return Template{} + } + domainNames, _ := m.DomainNames.AsStringArray() - t := Template{ + return Template{ ID: m.ID, CreatedOn: m.CreatedOn.Time.String(), ModifiedOn: m.ModifiedOn.Time.String(), @@ -288,8 +293,6 @@ func (m *Model) GetTemplate() Template { IsProvided: m.ID > 0 && m.Status == StatusProvided, Folder: m.GetFolder(), } - - return t } // GetFolder returns the folder where these certs should exist diff --git a/backend/internal/entity/certificateauthority/methods.go b/backend/internal/entity/certificateauthority/methods.go index 28941e2..9d0a14e 100644 --- a/backend/internal/entity/certificateauthority/methods.go +++ b/backend/internal/entity/certificateauthority/methods.go @@ -112,7 +112,7 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) } // Get rows - var items []Model + items := make([]Model, 0) query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) err := db.Select(&items, query, params...) if err != nil { diff --git a/backend/internal/entity/dnsprovider/methods.go b/backend/internal/entity/dnsprovider/methods.go index c1a1e0e..c31c742 100644 --- a/backend/internal/entity/dnsprovider/methods.go +++ b/backend/internal/entity/dnsprovider/methods.go @@ -112,7 +112,7 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) } // Get rows - var items []Model + items := make([]Model, 0) query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) err := db.Select(&items, query, params...) if err != nil { diff --git a/backend/internal/entity/host/methods.go b/backend/internal/entity/host/methods.go index 1de2a7d..68f00fd 100644 --- a/backend/internal/entity/host/methods.go +++ b/backend/internal/entity/host/methods.go @@ -34,7 +34,7 @@ func create(host *Model) (int, error) { modified_on, user_id, type, - host_template_id, + nginx_template_id, listen_interface, domain_names, upstream_id, @@ -48,7 +48,6 @@ func create(host *Model) (int, error) { hsts_enabled, hsts_subdomains, paths, - upstream_options, advanced_config, status, error_message, @@ -59,7 +58,7 @@ func create(host *Model) (int, error) { :modified_on, :user_id, :type, - :host_template_id, + :nginx_template_id, :listen_interface, :domain_names, :upstream_id, @@ -73,7 +72,6 @@ func create(host *Model) (int, error) { :hsts_enabled, :hsts_subdomains, :paths, - :upstream_options, :advanced_config, :status, :error_message, @@ -110,7 +108,7 @@ func update(host *Model) error { modified_on = :modified_on, user_id = :user_id, type = :type, - host_template_id = :host_template_id, + nginx_template_id = :nginx_template_id, listen_interface = :listen_interface, domain_names = :domain_names, upstream_id = :upstream_id, @@ -124,7 +122,6 @@ func update(host *Model) error { hsts_enabled = :hsts_enabled, hsts_subdomains = :hsts_subdomains, paths = :paths, - upstream_options = :upstream_options, advanced_config = :advanced_config, status = :status, error_message = :error_message, @@ -163,7 +160,7 @@ func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (Lis } // Get rows - var items []Model + items := make([]Model, 0) query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) err := db.Select(&items, query, params...) if err != nil { diff --git a/backend/internal/entity/host/model.go b/backend/internal/entity/host/model.go index 8b76836..31cf405 100644 --- a/backend/internal/entity/host/model.go +++ b/backend/internal/entity/host/model.go @@ -6,8 +6,10 @@ import ( "npm/internal/database" "npm/internal/entity/certificate" - "npm/internal/entity/hosttemplate" + "npm/internal/entity/nginxtemplate" + "npm/internal/entity/upstream" "npm/internal/entity/user" + "npm/internal/status" "npm/internal/types" "npm/internal/util" ) @@ -21,12 +23,6 @@ const ( RedirectionHostType = "redirection" // DeadHostType is self explanatory DeadHostType = "dead" - // StatusReady means a host is ready to configure - StatusReady = "ready" - // StatusOK means a host is configured within Nginx - StatusOK = "ok" - // StatusError is self explanatory - StatusError = "error" ) // Model is the user model @@ -36,7 +32,7 @@ type Model struct { ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` Type string `json:"type" db:"type" filter:"type,string"` - HostTemplateID int `json:"host_template_id" db:"host_template_id" filter:"host_template_id,integer"` + NginxTemplateID int `json:"nginx_template_id" db:"nginx_template_id" filter:"nginx_template_id,integer"` ListenInterface string `json:"listen_interface" db:"listen_interface" filter:"listen_interface,string"` DomainNames types.JSONB `json:"domain_names" db:"domain_names" filter:"domain_names,string"` UpstreamID int `json:"upstream_id" db:"upstream_id" filter:"upstream_id,integer"` @@ -50,16 +46,16 @@ type Model struct { HSTSEnabled bool `json:"hsts_enabled" db:"hsts_enabled" filter:"hsts_enabled,boolean"` HSTSSubdomains bool `json:"hsts_subdomains" db:"hsts_subdomains" filter:"hsts_subdomains,boolean"` Paths string `json:"paths" db:"paths" filter:"paths,string"` - UpstreamOptions string `json:"upstream_options" db:"upstream_options" filter:"upstream_options,string"` AdvancedConfig string `json:"advanced_config" db:"advanced_config" filter:"advanced_config,string"` Status string `json:"status" db:"status" filter:"status,string"` ErrorMessage string `json:"error_message" db:"error_message" filter:"error_message,string"` IsDisabled bool `json:"is_disabled" db:"is_disabled" filter:"is_disabled,boolean"` IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` // Expansions - Certificate *certificate.Model `json:"certificate,omitempty"` - HostTemplate *hosttemplate.Model `json:"host_template,omitempty"` - User *user.Model `json:"user,omitempty"` + Certificate *certificate.Model `json:"certificate,omitempty"` + NginxTemplate *nginxtemplate.Model `json:"nginx_template,omitempty"` + User *user.Model `json:"user,omitempty"` + Upstream *upstream.Model `json:"upstream,omitempty"` } func (m *Model) getByQuery(query string, params []interface{}) error { @@ -93,7 +89,7 @@ func (m *Model) Save(skipConfiguration bool) error { if !skipConfiguration { // Set this host as requiring reconfiguration - m.Status = StatusReady + m.Status = status.StatusReady } if m.ID == 0 { @@ -119,6 +115,13 @@ func (m *Model) Delete() bool { func (m *Model) Expand(items []string) error { var err error + // Always expand the upstream + if m.UpstreamID > 0 { + var u upstream.Model + u, err = upstream.GetByID(m.UpstreamID) + m.Upstream = &u + } + if util.SliceContainsItem(items, "user") && m.ID > 0 { var usr user.Model usr, err = user.GetByID(m.UserID) @@ -131,10 +134,10 @@ func (m *Model) Expand(items []string) error { m.Certificate = &cert } - if util.SliceContainsItem(items, "hosttemplate") && m.HostTemplateID > 0 { - var templ hosttemplate.Model - templ, err = hosttemplate.GetByID(m.HostTemplateID) - m.HostTemplate = &templ + if util.SliceContainsItem(items, "nginxtemplate") && m.NginxTemplateID > 0 { + var templ nginxtemplate.Model + templ, err = nginxtemplate.GetByID(m.NginxTemplateID) + m.NginxTemplate = &templ } return err @@ -150,7 +153,7 @@ func (m *Model) GetTemplate() Template { ModifiedOn: m.ModifiedOn.Time.String(), UserID: m.UserID, Type: m.Type, - HostTemplateID: m.HostTemplateID, + NginxTemplateID: m.NginxTemplateID, ListenInterface: m.ListenInterface, DomainNames: domainNames, UpstreamID: m.UpstreamID, @@ -164,7 +167,6 @@ func (m *Model) GetTemplate() Template { HSTSEnabled: m.HSTSEnabled, HSTSSubdomains: m.HSTSSubdomains, Paths: m.Paths, - UpstreamOptions: m.UpstreamOptions, AdvancedConfig: m.AdvancedConfig, Status: m.Status, ErrorMessage: m.ErrorMessage, diff --git a/backend/internal/entity/host/template.go b/backend/internal/entity/host/template.go index 06fe26d..957b977 100644 --- a/backend/internal/entity/host/template.go +++ b/backend/internal/entity/host/template.go @@ -1,5 +1,14 @@ package host +type TemplateUpstream struct { + Hostname string + Port int + BalanceMethod string + MaxFails int + FailTimeout int + AdvancedConfig string +} + // Template is the model given to the template parser, converted from the Model type Template struct { ID int @@ -7,7 +16,7 @@ type Template struct { ModifiedOn string UserID int Type string - HostTemplateID int + NginxTemplateID int ListenInterface string DomainNames []string UpstreamID int @@ -22,8 +31,8 @@ type Template struct { HSTSSubdomains bool IsDisabled bool Paths string - UpstreamOptions string AdvancedConfig string Status string ErrorMessage string + Upstreams []TemplateUpstream } diff --git a/backend/internal/entity/nginxtemplate/filters.go b/backend/internal/entity/nginxtemplate/filters.go new file mode 100644 index 0000000..91fd52a --- /dev/null +++ b/backend/internal/entity/nginxtemplate/filters.go @@ -0,0 +1,25 @@ +package nginxtemplate + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/hosttemplate/methods.go b/backend/internal/entity/nginxtemplate/methods.go similarity index 98% rename from backend/internal/entity/hosttemplate/methods.go rename to backend/internal/entity/nginxtemplate/methods.go index e75fcfd..70faef7 100644 --- a/backend/internal/entity/hosttemplate/methods.go +++ b/backend/internal/entity/nginxtemplate/methods.go @@ -1,4 +1,4 @@ -package hosttemplate +package nginxtemplate import ( "database/sql" @@ -108,7 +108,7 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) } // Get rows - var items []Model + items := make([]Model, 0) query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) err := db.Select(&items, query, params...) if err != nil { diff --git a/backend/internal/entity/hosttemplate/model.go b/backend/internal/entity/nginxtemplate/model.go similarity index 90% rename from backend/internal/entity/hosttemplate/model.go rename to backend/internal/entity/nginxtemplate/model.go index c791537..ecaac00 100644 --- a/backend/internal/entity/hosttemplate/model.go +++ b/backend/internal/entity/nginxtemplate/model.go @@ -1,4 +1,4 @@ -package hosttemplate +package nginxtemplate import ( "fmt" @@ -9,7 +9,7 @@ import ( ) const ( - tableName = "host_template" + tableName = "nginx_template" ) // Model is the user model @@ -19,7 +19,7 @@ type Model struct { ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` Name string `json:"name" db:"name" filter:"name,string"` - Type string `json:"host_type" db:"host_type" filter:"host_type,string"` + Type string `json:"type" db:"type" filter:"type,string"` Template string `json:"template" db:"template" filter:"template,string"` IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` } @@ -62,7 +62,7 @@ func (m *Model) Save() error { return err } -// Delete will mark a host as deleted +// Delete will mark a template as deleted func (m *Model) Delete() bool { m.Touch(false) m.IsDeleted = true diff --git a/backend/internal/entity/nginxtemplate/structs.go b/backend/internal/entity/nginxtemplate/structs.go new file mode 100644 index 0000000..09999d1 --- /dev/null +++ b/backend/internal/entity/nginxtemplate/structs.go @@ -0,0 +1,15 @@ +package nginxtemplate + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for this list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/setting/methods.go b/backend/internal/entity/setting/methods.go index 51b7517..1d9eef9 100644 --- a/backend/internal/entity/setting/methods.go +++ b/backend/internal/entity/setting/methods.go @@ -106,7 +106,7 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) } // Get rows - var items []Model + items := make([]Model, 0) query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) err := db.Select(&items, query, params...) if err != nil { diff --git a/backend/internal/entity/stream/methods.go b/backend/internal/entity/stream/methods.go index 02ddd4d..0accd50 100644 --- a/backend/internal/entity/stream/methods.go +++ b/backend/internal/entity/stream/methods.go @@ -114,7 +114,7 @@ func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) } // Get rows - var items []Model + items := make([]Model, 0) query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) err := db.Select(&items, query, params...) if err != nil { diff --git a/backend/internal/entity/hosttemplate/filters.go b/backend/internal/entity/upstream/filters.go similarity index 97% rename from backend/internal/entity/hosttemplate/filters.go rename to backend/internal/entity/upstream/filters.go index 3b4d512..82ace11 100644 --- a/backend/internal/entity/hosttemplate/filters.go +++ b/backend/internal/entity/upstream/filters.go @@ -1,4 +1,4 @@ -package hosttemplate +package upstream import ( "npm/internal/entity" diff --git a/backend/internal/entity/upstream/methods.go b/backend/internal/entity/upstream/methods.go new file mode 100644 index 0000000..4418db7 --- /dev/null +++ b/backend/internal/entity/upstream/methods.go @@ -0,0 +1,162 @@ +package upstream + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a Upstream by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// create will create a Upstream from this model +func create(u *Model) (int, error) { + if u.ID != 0 { + return 0, goerrors.New("Cannot create upstream when model already has an ID") + } + + u.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + user_id, + name, + nginx_template_id, + ip_hash, + ntlm, + keepalive, + keepalive_requests, + keepalive_time, + keepalive_timeout, + advanced_config, + status, + error_message, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :user_id, + :name, + :nginx_template_id, + :ip_hash, + :ntlm, + :keepalive, + :keepalive_requests, + :keepalive_time, + :keepalive_timeout, + :advanced_config, + :status, + :error_message, + :is_deleted + )`, u) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + logger.Debug("Created Upstream: %+v", u) + + return int(last), nil +} + +// update will Update a Upstream from this model +func update(u *Model) error { + if u.ID == 0 { + return goerrors.New("Cannot update upstream when model doesn't have an ID") + } + + u.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + user_id = :user_id, + name = :name, + nginx_template_id = :nginx_template_id, + ip_hash = :ip_hash, + ntlm = :ntlm, + keepalive = :keepalive, + keepalive_requests = :keepalive_requests, + keepalive_time = :keepalive_time, + advanced_config = :advanced_config, + status = :status, + error_message = :error_message, + is_deleted = :is_deleted + WHERE id = :id`, u) + + logger.Debug("Updated Upstream: %+v", u) + + return err +} + +// List will return a list of Upstreams +func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "name", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + items := make([]Model, 0) + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%s -- %+v", query, params) + return result, err + } + + // Expand to get servers, at a minimum + for idx := range items { + // nolint: errcheck, gosec + items[idx].Expand(expand) + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/upstream/model.go b/backend/internal/entity/upstream/model.go new file mode 100644 index 0000000..408a878 --- /dev/null +++ b/backend/internal/entity/upstream/model.go @@ -0,0 +1,127 @@ +package upstream + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/entity/nginxtemplate" + "npm/internal/entity/upstreamserver" + "npm/internal/status" + "npm/internal/types" + "npm/internal/util" +) + +const ( + tableName = "upstream" +) + +// Model is the Upstream model +// See: http://nginx.org/en/docs/http/ngx_http_upstream_module.html#upstream +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + UserID int `json:"user_id" db:"user_id" filter:"user_id,integer"` + Name string `json:"name" db:"name" filter:"name,string"` + NginxTemplateID int `json:"nginx_template_id" db:"nginx_template_id" filter:"nginx_template_id,integer"` + IPHash bool `json:"ip_hash" db:"ip_hash" filter:"ip_hash,boolean"` + NTLM bool `json:"ntlm" db:"ntlm" filter:"ntlm,boolean"` + Keepalive int `json:"keepalive" db:"keepalive" filter:"keepalive,integer"` + KeepaliveRequests int `json:"keepalive_requests" db:"keepalive_requests" filter:"keepalive_requests,integer"` + KeepaliveTime string `json:"keepalive_time" db:"keepalive_time" filter:"keepalive_time,string"` + KeepaliveTimeout string `json:"keepalive_timeout" db:"keepalive_timeout" filter:"keepalive_timeout,string"` + AdvancedConfig string `json:"advanced_config" db:"advanced_config" filter:"advanced_config,string"` + Status string `json:"status" db:"status" filter:"status,string"` + ErrorMessage string `json:"error_message" db:"error_message" filter:"error_message,string"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` + // Expansions + Servers []upstreamserver.Model `json:"servers"` + NginxTemplate *nginxtemplate.Model `json:"nginx_template,omitempty"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + err := m.getByQuery(query, params) + if err == nil { + err = m.Expand(nil) + } + return err +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save(skipConfiguration bool) error { + var err error + + if m.UserID == 0 { + return fmt.Errorf("User ID must be specified") + } + + if !skipConfiguration { + // Set this upstream as requiring reconfiguration + m.Status = status.StatusReady + } + + if m.ID == 0 { + m.ID, err = create(m) + } else { + err = update(m) + } + + // Save Servers + if err == nil { + for idx := range m.Servers { + // Continue if previous iteration didn't cause an error + if err == nil { + m.Servers[idx].UpstreamID = m.ID + err = m.Servers[idx].Save() + } + } + } + + return err +} + +// Delete will mark a upstream as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(false); err != nil { + return false + } + return true +} + +// Expand will fill in more properties +func (m *Model) Expand(items []string) error { + var err error + + // Always expand servers, if not done already + if len(m.Servers) == 0 { + m.Servers, err = upstreamserver.GetByUpstreamID(m.ID) + } + + if util.SliceContainsItem(items, "nginxtemplate") && m.NginxTemplateID > 0 { + var templ nginxtemplate.Model + templ, err = nginxtemplate.GetByID(m.NginxTemplateID) + m.NginxTemplate = &templ + } + + return err +} diff --git a/backend/internal/entity/hosttemplate/structs.go b/backend/internal/entity/upstream/structs.go similarity index 94% rename from backend/internal/entity/hosttemplate/structs.go rename to backend/internal/entity/upstream/structs.go index 4cc54b9..b56dd06 100644 --- a/backend/internal/entity/hosttemplate/structs.go +++ b/backend/internal/entity/upstream/structs.go @@ -1,4 +1,4 @@ -package hosttemplate +package upstream import ( "npm/internal/model" diff --git a/backend/internal/entity/upstreamserver/filters.go b/backend/internal/entity/upstreamserver/filters.go new file mode 100644 index 0000000..e61197e --- /dev/null +++ b/backend/internal/entity/upstreamserver/filters.go @@ -0,0 +1,25 @@ +package upstreamserver + +import ( + "npm/internal/entity" +) + +var filterMapFunctions = make(map[string]entity.FilterMapFunction) + +// getFilterMapFunctions is a map of functions that should be executed +// during the filtering process, if a field is defined here then the value in +// the filter will be given to the defined function and it will return a new +// value for use in the sql query. +func getFilterMapFunctions() map[string]entity.FilterMapFunction { + // if len(filterMapFunctions) == 0 { + // TODO: See internal/model/file_item.go:620 for an example + // } + + return filterMapFunctions +} + +// GetFilterSchema returns filter schema +func GetFilterSchema() string { + var m Model + return entity.GetFilterSchema(m) +} diff --git a/backend/internal/entity/upstreamserver/methods.go b/backend/internal/entity/upstreamserver/methods.go new file mode 100644 index 0000000..69a12c5 --- /dev/null +++ b/backend/internal/entity/upstreamserver/methods.go @@ -0,0 +1,154 @@ +package upstreamserver + +import ( + "database/sql" + goerrors "errors" + "fmt" + + "npm/internal/database" + "npm/internal/entity" + "npm/internal/errors" + "npm/internal/logger" + "npm/internal/model" +) + +// GetByID finds a Upstream Server by ID +func GetByID(id int) (Model, error) { + var m Model + err := m.LoadByID(id) + return m, err +} + +// GetByUpstreamID finds all servers in the upstream +func GetByUpstreamID(upstreamID int) ([]Model, error) { + items := make([]Model, 0) + query := `SELECT * FROM ` + fmt.Sprintf("`%s`", tableName) + ` WHERE upstream_id = ? ORDER BY server` + db := database.GetInstance() + err := db.Select(&items, query, upstreamID) + if err != nil { + logger.Debug("%s -- %d", query, upstreamID) + } + return items, err +} + +// create will create a Upstream Server from this model +func create(u *Model) (int, error) { + if u.ID != 0 { + return 0, goerrors.New("Cannot create upstream server when model already has an ID") + } + + u.Touch(true) + + db := database.GetInstance() + // nolint: gosec + result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` ( + created_on, + modified_on, + upstream_id, + server, + weight, + max_conns, + max_fails, + fail_timeout, + backup, + is_deleted + ) VALUES ( + :created_on, + :modified_on, + :upstream_id, + :server, + :weight, + :max_conns, + :max_fails, + :fail_timeout, + :backup, + :is_deleted + )`, u) + + if err != nil { + return 0, err + } + + last, lastErr := result.LastInsertId() + if lastErr != nil { + return 0, lastErr + } + + logger.Debug("Created Upstream Server: %+v", u) + + return int(last), nil +} + +// update will Update a Upstream from this model +func update(u *Model) error { + if u.ID == 0 { + return goerrors.New("Cannot update upstream server when model doesn't have an ID") + } + + u.Touch(false) + + db := database.GetInstance() + // nolint: gosec + _, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET + created_on = :created_on, + modified_on = :modified_on, + upstream_id = :upstream_id, + server = :server, + weight = :weight, + max_conns = :max_conns, + max_fails = :max_fails, + fail_timeout = :fail_timeout, + backup = :backup, + is_deleted = :is_deleted + WHERE id = :id`, u) + + logger.Debug("Updated Upstream Server: %+v", u) + + return err +} + +// List will return a list of Upstreams +func List(pageInfo model.PageInfo, filters []model.Filter) (ListResponse, error) { + var result ListResponse + var exampleModel Model + + defaultSort := model.Sort{ + Field: "server", + Direction: "ASC", + } + + db := database.GetInstance() + if db == nil { + return result, errors.ErrDatabaseUnavailable + } + + // Get count of items in this search + query, params := entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), true) + countRow := db.QueryRowx(query, params...) + var totalRows int + queryErr := countRow.Scan(&totalRows) + if queryErr != nil && queryErr != sql.ErrNoRows { + logger.Debug("%s -- %+v", query, params) + return result, queryErr + } + + // Get rows + items := make([]Model, 0) + query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) + err := db.Select(&items, query, params...) + if err != nil { + logger.Debug("%s -- %+v", query, params) + return result, err + } + + result = ListResponse{ + Items: items, + Total: totalRows, + Limit: pageInfo.Limit, + Offset: pageInfo.Offset, + Sort: pageInfo.Sort, + Filter: filters, + } + + return result, nil +} diff --git a/backend/internal/entity/upstreamserver/model.go b/backend/internal/entity/upstreamserver/model.go new file mode 100644 index 0000000..50f7f23 --- /dev/null +++ b/backend/internal/entity/upstreamserver/model.go @@ -0,0 +1,76 @@ +package upstreamserver + +import ( + "fmt" + "time" + + "npm/internal/database" + "npm/internal/types" +) + +const ( + tableName = "upstream_server" +) + +// Model is the upstream model +type Model struct { + ID int `json:"id" db:"id" filter:"id,integer"` + CreatedOn types.DBDate `json:"created_on" db:"created_on" filter:"created_on,integer"` + ModifiedOn types.DBDate `json:"modified_on" db:"modified_on" filter:"modified_on,integer"` + UpstreamID int `json:"upstream_id" db:"upstream_id" filter:"upstream_id,integer"` + Server string `json:"server" db:"server" filter:"server,string"` + Weight int `json:"weight" db:"weight" filter:"weight,integer"` + MaxConns int `json:"max_conns" db:"max_conns" filter:"max_conns,integer"` + MaxFails int `json:"max_fails" db:"max_fails" filter:"max_fails,integer"` + FailTimeout int `json:"fail_timeout" db:"fail_timeout" filter:"fail_timeout,integer"` + Backup bool `json:"backup" db:"backup" filter:"backup,boolean"` + IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"` +} + +func (m *Model) getByQuery(query string, params []interface{}) error { + return database.GetByQuery(m, query, params) +} + +// LoadByID will load from an ID +func (m *Model) LoadByID(id int) error { + query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? AND is_deleted = ? LIMIT 1", tableName) + params := []interface{}{id, 0} + return m.getByQuery(query, params) +} + +// Touch will update model's timestamp(s) +func (m *Model) Touch(created bool) { + var d types.DBDate + d.Time = time.Now() + if created { + m.CreatedOn = d + } + m.ModifiedOn = d +} + +// Save will save this model to the DB +func (m *Model) Save() error { + var err error + + if m.UpstreamID == 0 { + return fmt.Errorf("Upstream ID must be specified") + } + + if m.ID == 0 { + m.ID, err = create(m) + } else { + err = update(m) + } + + return err +} + +// Delete will mark a upstream as deleted +func (m *Model) Delete() bool { + m.Touch(false) + m.IsDeleted = true + if err := m.Save(); err != nil { + return false + } + return true +} diff --git a/backend/internal/entity/upstreamserver/structs.go b/backend/internal/entity/upstreamserver/structs.go new file mode 100644 index 0000000..df6a40e --- /dev/null +++ b/backend/internal/entity/upstreamserver/structs.go @@ -0,0 +1,15 @@ +package upstreamserver + +import ( + "npm/internal/model" +) + +// ListResponse is the JSON response for this list +type ListResponse struct { + Total int `json:"total"` + Offset int `json:"offset"` + Limit int `json:"limit"` + Sort []model.Sort `json:"sort"` + Filter []model.Filter `json:"filter,omitempty"` + Items []Model `json:"items,omitempty"` +} diff --git a/backend/internal/entity/user/capabilities.go b/backend/internal/entity/user/capabilities.go index 704e7b6..a4c74b8 100644 --- a/backend/internal/entity/user/capabilities.go +++ b/backend/internal/entity/user/capabilities.go @@ -25,10 +25,10 @@ const ( CapabilityHostsView = "hosts.view" // CapabilityHostsManage hosts manage CapabilityHostsManage = "hosts.manage" - // CapabilityHostTemplatesView host-templates view - CapabilityHostTemplatesView = "host-templates.view" - // CapabilityHostTemplatesManage host-templates manage - CapabilityHostTemplatesManage = "host-templates.manage" + // CapabilityNginxTemplatesView nginx-templates view + CapabilityNginxTemplatesView = "nginx-templates.view" + // CapabilityNginxTemplatesManage nginx-templates manage + CapabilityNginxTemplatesManage = "nginx-templates.manage" // CapabilitySettingsManage settings manage CapabilitySettingsManage = "settings.manage" // CapabilityStreamsView streams view diff --git a/backend/internal/entity/user/methods.go b/backend/internal/entity/user/methods.go index 42b5247..c80890e 100644 --- a/backend/internal/entity/user/methods.go +++ b/backend/internal/entity/user/methods.go @@ -154,7 +154,7 @@ func List(pageInfo model.PageInfo, filters []model.Filter, expand []string) (Lis } // Get rows - var items []Model + items := make([]Model, 0) query, params = entity.ListQueryBuilder(exampleModel, tableName, &pageInfo, defaultSort, filters, getFilterMapFunctions(), false) err := db.Select(&items, query, params...) if err != nil { diff --git a/backend/internal/nginx/control.go b/backend/internal/nginx/control.go index a41c8e0..9d34ad2 100644 --- a/backend/internal/nginx/control.go +++ b/backend/internal/nginx/control.go @@ -4,47 +4,97 @@ import ( "fmt" "npm/internal/config" + "npm/internal/entity/certificate" "npm/internal/entity/host" + "npm/internal/entity/upstream" "npm/internal/logger" + "npm/internal/status" ) // ConfigureHost will attempt to write nginx conf and reload nginx func ConfigureHost(h host.Model) error { // nolint: errcheck, gosec - h.Expand([]string{"certificate", "hosttemplate"}) + h.Expand([]string{"certificate", "nginxtemplate"}) + + var certificateTemplate certificate.Template + if h.Certificate != nil { + certificateTemplate = h.Certificate.GetTemplate() + } data := TemplateData{ ConfDir: fmt.Sprintf("%s/nginx/hosts", config.Configuration.DataFolder), DataDir: config.Configuration.DataFolder, Host: h.GetTemplate(), - Certificate: h.Certificate.GetTemplate(), + Certificate: certificateTemplate, } filename := fmt.Sprintf("%s/host_%d.conf", data.ConfDir, h.ID) // Write the config to disk - err := writeTemplate(filename, h.HostTemplate.Template, data) + err := writeTemplate(filename, h.NginxTemplate.Template, data) if err != nil { // this configuration failed somehow - h.Status = host.StatusError + h.Status = status.StatusError h.ErrorMessage = fmt.Sprintf("Template generation failed: %s", err.Error()) logger.Debug(h.ErrorMessage) return h.Save(true) } // nolint: errcheck, gosec - if err := reloadNginx(); err != nil { + if output, err := reloadNginx(); err != nil { // reloading nginx failed, likely due to this host having a problem - h.Status = host.StatusError - h.ErrorMessage = fmt.Sprintf("Nginx configuation error: %s", err.Error()) + h.Status = status.StatusError + h.ErrorMessage = fmt.Sprintf("Nginx configuation error: %s - %s", err.Error(), output) writeConfigFile(filename, fmt.Sprintf("# %s", h.ErrorMessage)) logger.Debug(h.ErrorMessage) } else { // All good - h.Status = host.StatusOK + h.Status = status.StatusOK h.ErrorMessage = "" logger.Debug("ConfigureHost OK: %+v", h) } return h.Save(true) } + +// ConfigureUpstream will attempt to write nginx conf and reload nginx +func ConfigureUpstream(u upstream.Model) error { + logger.Debug("ConfigureUpstream: %+v)", u) + + // nolint: errcheck, gosec + u.Expand([]string{"nginxtemplate"}) + + data := TemplateData{ + ConfDir: fmt.Sprintf("%s/nginx/upstreams", config.Configuration.DataFolder), + DataDir: config.Configuration.DataFolder, + Upstream: u, + } + + filename := fmt.Sprintf("%s/upstream_%d.conf", data.ConfDir, u.ID) + + // Write the config to disk + err := writeTemplate(filename, u.NginxTemplate.Template, data) + if err != nil { + // this configuration failed somehow + u.Status = status.StatusError + u.ErrorMessage = fmt.Sprintf("Template generation failed: %s", err.Error()) + logger.Debug(u.ErrorMessage) + return u.Save(true) + } + + // nolint: errcheck, gosec + if output, err := reloadNginx(); err != nil { + // reloading nginx failed, likely due to this host having a problem + u.Status = status.StatusError + u.ErrorMessage = fmt.Sprintf("Nginx configuation error: %s - %s", err.Error(), output) + writeConfigFile(filename, fmt.Sprintf("# %s", u.ErrorMessage)) + logger.Debug(u.ErrorMessage) + } else { + // All good + u.Status = status.StatusOK + u.ErrorMessage = "" + logger.Debug("ConfigureUpstream OK: %+v", u) + } + + return u.Save(true) +} diff --git a/backend/internal/nginx/exec.go b/backend/internal/nginx/exec.go index 7801751..727aad3 100644 --- a/backend/internal/nginx/exec.go +++ b/backend/internal/nginx/exec.go @@ -7,9 +7,8 @@ import ( "npm/internal/logger" ) -func reloadNginx() error { - _, err := shExec([]string{"-s", "reload"}) - return err +func reloadNginx() (string, error) { + return shExec([]string{"-s", "reload"}) } func getNginxFilePath() (string, error) { @@ -32,7 +31,7 @@ func shExec(args []string) (string, error) { // nolint: gosec c := exec.Command(ng, args...) - b, e := c.Output() + b, e := c.CombinedOutput() if e != nil { logger.Error("NginxError", fmt.Errorf("Command error: %s -- %v\n%+v", ng, args, e)) diff --git a/backend/internal/nginx/templates.go b/backend/internal/nginx/templates.go index 09285fd..951cc42 100644 --- a/backend/internal/nginx/templates.go +++ b/backend/internal/nginx/templates.go @@ -6,7 +6,9 @@ import ( "npm/internal/entity/certificate" "npm/internal/entity/host" + "npm/internal/entity/upstream" "npm/internal/logger" + "npm/internal/util" "github.com/aymerick/raymond" ) @@ -17,10 +19,15 @@ type TemplateData struct { DataDir string Host host.Template Certificate certificate.Template + Upstream upstream.Model } func generateHostConfig(template string, data TemplateData) (string, error) { + logger.Debug("Rendering Template - Template: %s", template) + logger.Debug("Rendering Template - Data: %+v", data) return raymond.Render(template, data) + + // todo: apply some post processing to this config, stripe trailing whitespace from lines and then remove groups of 2+ \n's so the config looks nicer } func writeTemplate(filename, template string, data TemplateData) error { @@ -31,7 +38,7 @@ func writeTemplate(filename, template string, data TemplateData) error { // Write it. This will also write an error comment if generation failed // nolint: gosec - writeErr := writeConfigFile(filename, output) + writeErr := writeConfigFile(filename, util.CleanupWhitespace(output)) if err != nil { return err } diff --git a/backend/internal/status/status.go b/backend/internal/status/status.go new file mode 100644 index 0000000..84f8dc6 --- /dev/null +++ b/backend/internal/status/status.go @@ -0,0 +1,10 @@ +package status + +const ( + // StatusReady means a host is ready to configure + StatusReady = "ready" + // StatusOK means a host is configured within Nginx + StatusOK = "ok" + // StatusError is self explanatory + StatusError = "error" +) diff --git a/backend/internal/util/strings.go b/backend/internal/util/strings.go new file mode 100644 index 0000000..085602b --- /dev/null +++ b/backend/internal/util/strings.go @@ -0,0 +1,24 @@ +package util + +import ( + "regexp" + "strings" + "unicode" +) + +// CleanupWhitespace will trim up and remove extra lines and stuff +func CleanupWhitespace(s string) string { + // Remove trailing whitespace from all lines + slices := strings.Split(s, "\n") + for idx := range slices { + slices[idx] = strings.TrimRightFunc(slices[idx], unicode.IsSpace) + } + // Output: [a b c] + result := strings.Join(slices, "\n") + + // Remove empty lines + r1 := regexp.MustCompile("\n+") + result = r1.ReplaceAllString(result, "\n") + + return result +} diff --git a/backend/internal/util/strings_test.go b/backend/internal/util/strings_test.go new file mode 100644 index 0000000..d9bd24b --- /dev/null +++ b/backend/internal/util/strings_test.go @@ -0,0 +1,51 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCleanupWhitespace(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "test a", + input: `# ------------------------------------------------------------ +# Upstream 5: API servers 2 +# ------------------------------------------------------------ + +upstream npm_upstream_5 {` + ` ` + /* this adds whitespace to the end without the ide trimming it */ ` + + + + + + + + + + server 192.168.0.10:80 weight=100 ; + server 192.168.0.11:80 weight=50 ; + +}`, + want: `# ------------------------------------------------------------ +# Upstream 5: API servers 2 +# ------------------------------------------------------------ +upstream npm_upstream_5 { + server 192.168.0.10:80 weight=100 ; + server 192.168.0.11:80 weight=50 ; +}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + output := CleanupWhitespace(tt.input) + assert.Equal(t, tt.want, output) + }) + } +} diff --git a/backend/internal/validator/hosts.go b/backend/internal/validator/hosts.go index f830d2d..7a7ca2e 100644 --- a/backend/internal/validator/hosts.go +++ b/backend/internal/validator/hosts.go @@ -5,7 +5,7 @@ import ( "npm/internal/entity/certificate" "npm/internal/entity/host" - "npm/internal/entity/hosttemplate" + "npm/internal/entity/nginxtemplate" ) // ValidateHost will check if associated objects exist and other checks @@ -20,13 +20,13 @@ func ValidateHost(h host.Model) error { } } - // Check the host template exists and has the same type. - hostTemplate, tErr := hosttemplate.GetByID(h.HostTemplateID) + // Check the nginx template exists and has the same type. + nginxTemplate, tErr := nginxtemplate.GetByID(h.NginxTemplateID) if tErr != nil { - return fmt.Errorf("Host Template #%d does not exist", h.HostTemplateID) + return fmt.Errorf("Host Template #%d does not exist", h.NginxTemplateID) } - if hostTemplate.Type != h.Type { - return fmt.Errorf("Host Template #%d is not valid for this host type", h.HostTemplateID) + if nginxTemplate.Type != h.Type { + return fmt.Errorf("Host Template #%d is not valid for this host type", h.NginxTemplateID) } return nil diff --git a/backend/internal/validator/upstreams.go b/backend/internal/validator/upstreams.go new file mode 100644 index 0000000..c8c9648 --- /dev/null +++ b/backend/internal/validator/upstreams.go @@ -0,0 +1,39 @@ +package validator + +import ( + "errors" + "fmt" + + "npm/internal/entity/nginxtemplate" + "npm/internal/entity/upstream" +) + +// ValidateUpstream will check if associated objects exist and other checks +// will return a nil error if things are OK +func ValidateUpstream(u upstream.Model) error { + // Needs to have more than 1 server + if len(u.Servers) < 2 { + return errors.New("Upstreams require at least 2 servers") + } + + // Backup servers aren't permitted with hash balancing + if u.IPHash { + // check all servers for a backup param + for _, server := range u.Servers { + if server.Backup { + return errors.New("Backup servers cannot be used with hash balancing") + } + } + } + + // Check the nginx template exists and has the same type. + nginxTemplate, err := nginxtemplate.GetByID(u.NginxTemplateID) + if err != nil { + return fmt.Errorf("Nginx Template #%d does not exist", u.NginxTemplateID) + } + if nginxTemplate.Type != "upstream" { + return fmt.Errorf("Host Template #%d is not valid for this upstream", u.NginxTemplateID) + } + + return nil +} diff --git a/backend/scripts/test.sh b/backend/scripts/test.sh index 92a0a02..dcfb5c5 100755 --- a/backend/scripts/test.sh +++ b/backend/scripts/test.sh @@ -1,5 +1,3 @@ #!/bin/bash -e -export RICHGO_FORCE_COLOR=1 - -richgo test -bench=. -cover -v ./internal/... +go test -json -cover ./internal/... | tparse diff --git a/docker/rootfs/etc/nginx/nginx.conf b/docker/rootfs/etc/nginx/nginx.conf index f815c1b..87118ad 100644 --- a/docker/rootfs/etc/nginx/nginx.conf +++ b/docker/rootfs/etc/nginx/nginx.conf @@ -1,6 +1,6 @@ # run nginx in foreground daemon off; -user npmuser; +#user npmuser; pid /run/nginx/nginx.pid; # Set number of worker processes automatically based on number of CPU cores. @@ -60,7 +60,7 @@ http { # Files generated by NPM include /etc/nginx/conf.d/*.conf; include /data/nginx/default_host/*.conf; - include /data/nginx/proxy_host/*.conf; - include /data/nginx/redirection_host/*.conf; - include /data/nginx/dead_host/*.conf; + include /data/nginx/upstreams/*.conf; + include /data/nginx/hosts/*.conf; + include /data/nginx/streams/*.conf; } diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 755579a..d4796e5 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -14,7 +14,7 @@ const CertificateAuthorities = lazy( const Dashboard = lazy(() => import("pages/Dashboard")); const DNSProviders = lazy(() => import("pages/DNSProviders")); const Hosts = lazy(() => import("pages/Hosts")); -const HostTemplates = lazy(() => import("pages/HostTemplates")); +const NginxTemplates = lazy(() => import("pages/NginxTemplates")); const Login = lazy(() => import("pages/Login")); const GeneralSettings = lazy(() => import("pages/Settings")); const Setup = lazy(() => import("pages/Setup")); @@ -66,8 +66,8 @@ function Router() { } /> } /> } + path="/settings/nginx-templates" + element={} /> } /> } /> diff --git a/frontend/src/api/npm/getHostTemplates.ts b/frontend/src/api/npm/getNginxTemplates.ts similarity index 63% rename from frontend/src/api/npm/getHostTemplates.ts rename to frontend/src/api/npm/getNginxTemplates.ts index dc91d66..15d8e28 100644 --- a/frontend/src/api/npm/getHostTemplates.ts +++ b/frontend/src/api/npm/getNginxTemplates.ts @@ -1,16 +1,16 @@ import * as api from "./base"; -import { HostTemplatesResponse } from "./responseTypes"; +import { NginxTemplatesResponse } from "./responseTypes"; -export async function getHostTemplates( +export async function getNginxTemplates( offset = 0, limit = 10, sort?: string, filters?: { [key: string]: string }, abortController?: AbortController, -): Promise { +): Promise { const { result } = await api.get( { - url: "host-templates", + url: "nginx-templates", params: { limit, offset, sort, ...filters }, }, abortController, diff --git a/frontend/src/api/npm/index.ts b/frontend/src/api/npm/index.ts index faf6fe8..d03fa7a 100644 --- a/frontend/src/api/npm/index.ts +++ b/frontend/src/api/npm/index.ts @@ -9,7 +9,7 @@ export * from "./getDNSProviders"; export * from "./getDNSProvidersAcmesh"; export * from "./getHealth"; export * from "./getHosts"; -export * from "./getHostTemplates"; +export * from "./getNginxTemplates"; export * from "./getSettings"; export * from "./getToken"; export * from "./getUser"; diff --git a/frontend/src/api/npm/models.ts b/frontend/src/api/npm/models.ts index b739ecd..4f043a1 100644 --- a/frontend/src/api/npm/models.ts +++ b/frontend/src/api/npm/models.ts @@ -94,7 +94,7 @@ export interface Host { modifiedOn: number; userId: number; type: string; - hostTemplateId: number; + nginxTemplateId: number; listenInterface: number; domainNames: string[]; upstreamId: number; @@ -108,16 +108,15 @@ export interface Host { hstsEnabled: boolean; hstsSubdomains: boolean; paths: string; - upstreamOptions: string; advancedConfig: string; isDisabled: boolean; } -export interface HostTemplate { +export interface NginxTemplate { id: number; createdOn: number; modifiedOn: number; userId: number; - hostType: string; + type: string; template: string; } diff --git a/frontend/src/api/npm/responseTypes.ts b/frontend/src/api/npm/responseTypes.ts index a15276a..7d66204 100644 --- a/frontend/src/api/npm/responseTypes.ts +++ b/frontend/src/api/npm/responseTypes.ts @@ -3,7 +3,7 @@ import { CertificateAuthority, DNSProvider, Host, - HostTemplate, + NginxTemplate, Setting, Sort, User, @@ -53,6 +53,6 @@ export interface HostsResponse extends BaseResponse { items: Host[]; } -export interface HostTemplatesResponse extends BaseResponse { - items: HostTemplate[]; +export interface NginxTemplatesResponse extends BaseResponse { + items: NginxTemplate[]; } diff --git a/frontend/src/components/Navigation/NavigationMenu.tsx b/frontend/src/components/Navigation/NavigationMenu.tsx index bc7575e..62edb3b 100644 --- a/frontend/src/components/Navigation/NavigationMenu.tsx +++ b/frontend/src/components/Navigation/NavigationMenu.tsx @@ -51,7 +51,16 @@ const navItems: NavItem[] = [ { label: intl.formatMessage({ id: "hosts.title" }), icon: , - to: "/hosts", + subItems: [ + { + label: intl.formatMessage({ id: "hosts.title" }), + to: "/hosts", + }, + { + label: intl.formatMessage({ id: "upstreams.title" }), + to: "/upstreams", + }, + ], }, { label: intl.formatMessage({ id: "access-lists.title" }), @@ -95,8 +104,8 @@ const navItems: NavItem[] = [ to: "/settings/general", }, { - label: intl.formatMessage({ id: "host-templates.title" }), - to: "/settings/host-templates", + label: intl.formatMessage({ id: "nginx-templates.title" }), + to: "/settings/nginx-templates", }, ], }, diff --git a/frontend/src/components/Permissions/PermissionSelector.tsx b/frontend/src/components/Permissions/PermissionSelector.tsx index 146c782..45e7556 100644 --- a/frontend/src/components/Permissions/PermissionSelector.tsx +++ b/frontend/src/components/Permissions/PermissionSelector.tsx @@ -212,13 +212,13 @@ function PermissionSelector({ - {intl.formatMessage({ id: "host-templates.title" })} + {intl.formatMessage({ id: "nginx-templates.title" })}